mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2025-04-17 18:18:11 +00:00
Add ability to add invitee to workspaces automatically (#975)
This commit is contained in:
parent
1cd255c1ec
commit
1ecefe8bed
10 changed files with 182 additions and 13 deletions
frontend/src
server
endpoints
models
prisma
swagger
utils
|
@ -64,10 +64,14 @@ const Admin = {
|
|||
return [];
|
||||
});
|
||||
},
|
||||
newInvite: async () => {
|
||||
newInvite: async ({ role = null, workspaceIds = null }) => {
|
||||
return await fetch(`${API_BASE}/admin/invite/new`, {
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
headers: baseHeaders(),
|
||||
body: JSON.stringify({
|
||||
role,
|
||||
workspaceIds,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.catch((e) => {
|
||||
|
|
|
@ -1,16 +1,23 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import Admin from "@/models/admin";
|
||||
import Workspace from "@/models/workspace";
|
||||
|
||||
export default function NewInviteModal({ closeModal }) {
|
||||
const [invite, setInvite] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [workspaces, setWorkspaces] = useState([]);
|
||||
const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState([]);
|
||||
|
||||
const handleCreate = async (e) => {
|
||||
setError(null);
|
||||
e.preventDefault();
|
||||
const { invite: newInvite, error } = await Admin.newInvite();
|
||||
|
||||
const { invite: newInvite, error } = await Admin.newInvite({
|
||||
role: null,
|
||||
workspaceIds: selectedWorkspaceIds,
|
||||
});
|
||||
if (!!newInvite) setInvite(newInvite);
|
||||
setError(error);
|
||||
};
|
||||
|
@ -21,6 +28,16 @@ export default function NewInviteModal({ closeModal }) {
|
|||
);
|
||||
setCopied(true);
|
||||
};
|
||||
|
||||
const handleWorkspaceSelection = (workspaceId) => {
|
||||
if (selectedWorkspaceIds.includes(workspaceId)) {
|
||||
const updated = selectedWorkspaceIds.filter((id) => id !== workspaceId);
|
||||
setSelectedWorkspaceIds(updated);
|
||||
return;
|
||||
}
|
||||
setSelectedWorkspaceIds([...selectedWorkspaceIds, workspaceId]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
function resetStatus() {
|
||||
if (!copied) return false;
|
||||
|
@ -31,6 +48,15 @@ export default function NewInviteModal({ closeModal }) {
|
|||
resetStatus();
|
||||
}, [copied]);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchWorkspaces() {
|
||||
Workspace.all()
|
||||
.then((workspaces) => setWorkspaces(workspaces))
|
||||
.catch(() => setWorkspaces([]));
|
||||
}
|
||||
fetchWorkspaces();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
||||
|
@ -61,11 +87,45 @@ export default function NewInviteModal({ closeModal }) {
|
|||
)}
|
||||
<p className="text-white text-xs md:text-sm">
|
||||
After creation you will be able to copy the invite and send it
|
||||
to a new user where they can create an account as a default
|
||||
user.
|
||||
to a new user where they can create an account as the{" "}
|
||||
<b>default</b> role and automatically be added to workspaces
|
||||
selected.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{workspaces.length > 0 && !invite && (
|
||||
<div className="p-6 flex w-full justify-between">
|
||||
<div className="w-full">
|
||||
<div className="flex flex-col gap-y-1 mb-2">
|
||||
<label
|
||||
htmlFor="workspaces"
|
||||
className="text-sm font-medium text-white"
|
||||
>
|
||||
Auto-add invitee to workspaces
|
||||
</label>
|
||||
<p className="text-white/60 text-xs">
|
||||
You can optionally automatically assign the user to the
|
||||
workspaces below by selecting them. By default, the user
|
||||
will not have any workspaces visible. You can assign
|
||||
workspaces later post-invite acceptance.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{workspaces.map((workspace) => (
|
||||
<WorkspaceOption
|
||||
key={workspace.id}
|
||||
workspace={workspace}
|
||||
selected={selectedWorkspaceIds.includes(workspace.id)}
|
||||
toggleSelection={handleWorkspaceSelection}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||
{!invite ? (
|
||||
<>
|
||||
|
@ -99,3 +159,31 @@ export default function NewInviteModal({ closeModal }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceOption({ workspace, selected, toggleSelection }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleSelection(workspace.id)}
|
||||
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||
selected ? "border-white border-opacity-40" : "border-none "
|
||||
} hover:border-white/60`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="workspace"
|
||||
value={workspace.id}
|
||||
checked={selected}
|
||||
className="hidden"
|
||||
/>
|
||||
<div
|
||||
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||
selected ? "bg-white" : ""
|
||||
}`}
|
||||
></div>
|
||||
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||
{workspace.name}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -165,13 +165,18 @@ function adminEndpoints(app) {
|
|||
}
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.post(
|
||||
"/admin/invite/new",
|
||||
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||
async (request, response) => {
|
||||
try {
|
||||
const user = await userFromSession(request, response);
|
||||
const { invite, error } = await Invite.create(user.id);
|
||||
const body = reqBody(request);
|
||||
const { invite, error } = await Invite.create({
|
||||
createdByUserId: user.id,
|
||||
workspaceIds: body?.workspaceIds || [],
|
||||
});
|
||||
|
||||
await EventLogs.logEvent(
|
||||
"invite_created",
|
||||
{
|
||||
|
|
|
@ -323,6 +323,18 @@ function apiAdminEndpoints(app) {
|
|||
/*
|
||||
#swagger.tags = ['Admin']
|
||||
#swagger.description = 'Create a new invite code for someone to use to register with instance. Methods are disabled until multi user mode is enabled via the UI.'
|
||||
#swagger.requestBody = {
|
||||
description: 'Request body for creation parameters of the invitation',
|
||||
required: false,
|
||||
type: 'object',
|
||||
content: {
|
||||
"application/json": {
|
||||
example: {
|
||||
workspaceIds: [1,2,45],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#swagger.responses[200] = {
|
||||
content: {
|
||||
"application/json": {
|
||||
|
@ -355,7 +367,10 @@ function apiAdminEndpoints(app) {
|
|||
return;
|
||||
}
|
||||
|
||||
const { invite, error } = await Invite.create();
|
||||
const body = reqBody(request);
|
||||
const { invite, error } = await Invite.create({
|
||||
workspaceIds: body?.workspaceIds ?? [],
|
||||
});
|
||||
response.status(200).json({ invite, error });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
const { safeJsonParse } = require("../utils/http");
|
||||
const prisma = require("../utils/prisma");
|
||||
|
||||
const Invite = {
|
||||
|
@ -6,12 +7,13 @@ const Invite = {
|
|||
return uuidAPIKey.create().apiKey;
|
||||
},
|
||||
|
||||
create: async function (createdByUserId = 0) {
|
||||
create: async function ({ createdByUserId = 0, workspaceIds = [] }) {
|
||||
try {
|
||||
const invite = await prisma.invites.create({
|
||||
data: {
|
||||
code: this.makeCode(),
|
||||
createdBy: createdByUserId,
|
||||
workspaceIds: JSON.stringify(workspaceIds),
|
||||
},
|
||||
});
|
||||
return { invite, error: null };
|
||||
|
@ -23,7 +25,7 @@ const Invite = {
|
|||
|
||||
deactivate: async function (inviteId = null) {
|
||||
try {
|
||||
const invite = await prisma.invites.update({
|
||||
await prisma.invites.update({
|
||||
where: { id: Number(inviteId) },
|
||||
data: { status: "disabled" },
|
||||
});
|
||||
|
@ -40,6 +42,26 @@ const Invite = {
|
|||
where: { id: Number(inviteId) },
|
||||
data: { status: "claimed", claimedBy: user.id },
|
||||
});
|
||||
|
||||
try {
|
||||
if (!!invite?.workspaceIds) {
|
||||
const { Workspace } = require("./workspace");
|
||||
const { WorkspaceUser } = require("./workspaceUsers");
|
||||
const workspaceIds = (await Workspace.where({})).map(
|
||||
(workspace) => workspace.id
|
||||
);
|
||||
const ids = safeJsonParse(invite.workspaceIds)
|
||||
.map((id) => Number(id))
|
||||
.filter((id) => workspaceIds.includes(id));
|
||||
if (ids.length !== 0) await WorkspaceUser.createMany(user.id, ids);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
"Could not add user to workspaces automatically",
|
||||
e.message
|
||||
);
|
||||
}
|
||||
|
||||
return { success: true, error: null };
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "invites" ADD COLUMN "workspaceIds" TEXT;
|
|
@ -41,6 +41,7 @@ model invites {
|
|||
code String @unique
|
||||
status String @default("pending")
|
||||
claimedBy Int?
|
||||
workspaceIds String?
|
||||
createdAt DateTime @default(now())
|
||||
createdBy Int
|
||||
lastUpdatedAt DateTime @default(now())
|
||||
|
@ -100,7 +101,7 @@ model workspaces {
|
|||
chatModel String?
|
||||
topN Int? @default(4)
|
||||
chatMode String? @default("chat")
|
||||
pfpFilename String?
|
||||
pfpFilename String?
|
||||
workspace_users workspace_users[]
|
||||
documents workspace_documents[]
|
||||
workspace_suggested_messages workspace_suggested_messages[]
|
||||
|
|
|
@ -489,6 +489,22 @@
|
|||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"description": "Request body for creation parameters of the invitation",
|
||||
"required": false,
|
||||
"type": "object",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"example": {
|
||||
"workspaceIds": [
|
||||
1,
|
||||
2,
|
||||
45
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -53,8 +53,16 @@ async function renameLogoFile(originalFilename = null) {
|
|||
const extname = path.extname(originalFilename) || ".png";
|
||||
const newFilename = `${v4()}${extname}`;
|
||||
const originalFilepath = process.env.STORAGE_DIR
|
||||
? path.join(process.env.STORAGE_DIR, "assets", normalizePath(originalFilename))
|
||||
: path.join(__dirname, `../../storage/assets`, normalizePath(originalFilename));
|
||||
? path.join(
|
||||
process.env.STORAGE_DIR,
|
||||
"assets",
|
||||
normalizePath(originalFilename)
|
||||
)
|
||||
: path.join(
|
||||
__dirname,
|
||||
`../../storage/assets`,
|
||||
normalizePath(originalFilename)
|
||||
);
|
||||
const outputFilepath = process.env.STORAGE_DIR
|
||||
? path.join(process.env.STORAGE_DIR, "assets", normalizePath(newFilename))
|
||||
: path.join(__dirname, `../../storage/assets`, normalizePath(newFilename));
|
||||
|
|
|
@ -61,6 +61,13 @@ function parseAuthHeader(headerValue = null, apiKey = null) {
|
|||
return { [headerValue]: apiKey };
|
||||
}
|
||||
|
||||
function safeJsonParse(jsonString, fallback = null) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch {}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
reqBody,
|
||||
multiUserMode,
|
||||
|
@ -69,4 +76,5 @@ module.exports = {
|
|||
decodeJWT,
|
||||
userFromSession,
|
||||
parseAuthHeader,
|
||||
safeJsonParse,
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue