Add ability to add invitee to workspaces automatically (#975)
This commit is contained in:
parent
1cd255c1ec
commit
1ecefe8bed
@ -64,10 +64,14 @@ const Admin = {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
newInvite: async () => {
|
newInvite: async ({ role = null, workspaceIds = null }) => {
|
||||||
return await fetch(`${API_BASE}/admin/invite/new`, {
|
return await fetch(`${API_BASE}/admin/invite/new`, {
|
||||||
method: "GET",
|
method: "POST",
|
||||||
headers: baseHeaders(),
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify({
|
||||||
|
role,
|
||||||
|
workspaceIds,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
@ -1,16 +1,23 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { X } from "@phosphor-icons/react";
|
import { X } from "@phosphor-icons/react";
|
||||||
import Admin from "@/models/admin";
|
import Admin from "@/models/admin";
|
||||||
|
import Workspace from "@/models/workspace";
|
||||||
|
|
||||||
export default function NewInviteModal({ closeModal }) {
|
export default function NewInviteModal({ closeModal }) {
|
||||||
const [invite, setInvite] = useState(null);
|
const [invite, setInvite] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [workspaces, setWorkspaces] = useState([]);
|
||||||
|
const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState([]);
|
||||||
|
|
||||||
const handleCreate = async (e) => {
|
const handleCreate = async (e) => {
|
||||||
setError(null);
|
setError(null);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { invite: newInvite, error } = await Admin.newInvite();
|
|
||||||
|
const { invite: newInvite, error } = await Admin.newInvite({
|
||||||
|
role: null,
|
||||||
|
workspaceIds: selectedWorkspaceIds,
|
||||||
|
});
|
||||||
if (!!newInvite) setInvite(newInvite);
|
if (!!newInvite) setInvite(newInvite);
|
||||||
setError(error);
|
setError(error);
|
||||||
};
|
};
|
||||||
@ -21,6 +28,16 @@ export default function NewInviteModal({ closeModal }) {
|
|||||||
);
|
);
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleWorkspaceSelection = (workspaceId) => {
|
||||||
|
if (selectedWorkspaceIds.includes(workspaceId)) {
|
||||||
|
const updated = selectedWorkspaceIds.filter((id) => id !== workspaceId);
|
||||||
|
setSelectedWorkspaceIds(updated);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedWorkspaceIds([...selectedWorkspaceIds, workspaceId]);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function resetStatus() {
|
function resetStatus() {
|
||||||
if (!copied) return false;
|
if (!copied) return false;
|
||||||
@ -31,6 +48,15 @@ export default function NewInviteModal({ closeModal }) {
|
|||||||
resetStatus();
|
resetStatus();
|
||||||
}, [copied]);
|
}, [copied]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchWorkspaces() {
|
||||||
|
Workspace.all()
|
||||||
|
.then((workspaces) => setWorkspaces(workspaces))
|
||||||
|
.catch(() => setWorkspaces([]));
|
||||||
|
}
|
||||||
|
fetchWorkspaces();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-[500px] max-w-2xl max-h-full">
|
<div className="relative w-[500px] max-w-2xl max-h-full">
|
||||||
<div className="relative bg-main-gradient rounded-lg shadow">
|
<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">
|
<p className="text-white text-xs md:text-sm">
|
||||||
After creation you will be able to copy the invite and send it
|
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
|
to a new user where they can create an account as the{" "}
|
||||||
user.
|
<b>default</b> role and automatically be added to workspaces
|
||||||
|
selected.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
||||||
{!invite ? (
|
{!invite ? (
|
||||||
<>
|
<>
|
||||||
@ -99,3 +159,31 @@ export default function NewInviteModal({ closeModal }) {
|
|||||||
</div>
|
</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",
|
"/admin/invite/new",
|
||||||
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
[validatedRequest, strictMultiUserRoleValid([ROLES.admin, ROLES.manager])],
|
||||||
async (request, response) => {
|
async (request, response) => {
|
||||||
try {
|
try {
|
||||||
const user = await userFromSession(request, response);
|
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(
|
await EventLogs.logEvent(
|
||||||
"invite_created",
|
"invite_created",
|
||||||
{
|
{
|
||||||
|
|||||||
@ -323,6 +323,18 @@ function apiAdminEndpoints(app) {
|
|||||||
/*
|
/*
|
||||||
#swagger.tags = ['Admin']
|
#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.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] = {
|
#swagger.responses[200] = {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
@ -355,7 +367,10 @@ function apiAdminEndpoints(app) {
|
|||||||
return;
|
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 });
|
response.status(200).json({ invite, error });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
const { safeJsonParse } = require("../utils/http");
|
||||||
const prisma = require("../utils/prisma");
|
const prisma = require("../utils/prisma");
|
||||||
|
|
||||||
const Invite = {
|
const Invite = {
|
||||||
@ -6,12 +7,13 @@ const Invite = {
|
|||||||
return uuidAPIKey.create().apiKey;
|
return uuidAPIKey.create().apiKey;
|
||||||
},
|
},
|
||||||
|
|
||||||
create: async function (createdByUserId = 0) {
|
create: async function ({ createdByUserId = 0, workspaceIds = [] }) {
|
||||||
try {
|
try {
|
||||||
const invite = await prisma.invites.create({
|
const invite = await prisma.invites.create({
|
||||||
data: {
|
data: {
|
||||||
code: this.makeCode(),
|
code: this.makeCode(),
|
||||||
createdBy: createdByUserId,
|
createdBy: createdByUserId,
|
||||||
|
workspaceIds: JSON.stringify(workspaceIds),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { invite, error: null };
|
return { invite, error: null };
|
||||||
@ -23,7 +25,7 @@ const Invite = {
|
|||||||
|
|
||||||
deactivate: async function (inviteId = null) {
|
deactivate: async function (inviteId = null) {
|
||||||
try {
|
try {
|
||||||
const invite = await prisma.invites.update({
|
await prisma.invites.update({
|
||||||
where: { id: Number(inviteId) },
|
where: { id: Number(inviteId) },
|
||||||
data: { status: "disabled" },
|
data: { status: "disabled" },
|
||||||
});
|
});
|
||||||
@ -40,6 +42,26 @@ const Invite = {
|
|||||||
where: { id: Number(inviteId) },
|
where: { id: Number(inviteId) },
|
||||||
data: { status: "claimed", claimedBy: user.id },
|
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 };
|
return { success: true, error: null };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error.message);
|
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
|
code String @unique
|
||||||
status String @default("pending")
|
status String @default("pending")
|
||||||
claimedBy Int?
|
claimedBy Int?
|
||||||
|
workspaceIds String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
createdBy Int
|
createdBy Int
|
||||||
lastUpdatedAt DateTime @default(now())
|
lastUpdatedAt DateTime @default(now())
|
||||||
@ -100,7 +101,7 @@ model workspaces {
|
|||||||
chatModel String?
|
chatModel String?
|
||||||
topN Int? @default(4)
|
topN Int? @default(4)
|
||||||
chatMode String? @default("chat")
|
chatMode String? @default("chat")
|
||||||
pfpFilename String?
|
pfpFilename String?
|
||||||
workspace_users workspace_users[]
|
workspace_users workspace_users[]
|
||||||
documents workspace_documents[]
|
documents workspace_documents[]
|
||||||
workspace_suggested_messages workspace_suggested_messages[]
|
workspace_suggested_messages workspace_suggested_messages[]
|
||||||
|
|||||||
@ -489,6 +489,22 @@
|
|||||||
"500": {
|
"500": {
|
||||||
"description": "Internal Server Error"
|
"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 extname = path.extname(originalFilename) || ".png";
|
||||||
const newFilename = `${v4()}${extname}`;
|
const newFilename = `${v4()}${extname}`;
|
||||||
const originalFilepath = process.env.STORAGE_DIR
|
const originalFilepath = process.env.STORAGE_DIR
|
||||||
? path.join(process.env.STORAGE_DIR, "assets", normalizePath(originalFilename))
|
? path.join(
|
||||||
: path.join(__dirname, `../../storage/assets`, normalizePath(originalFilename));
|
process.env.STORAGE_DIR,
|
||||||
|
"assets",
|
||||||
|
normalizePath(originalFilename)
|
||||||
|
)
|
||||||
|
: path.join(
|
||||||
|
__dirname,
|
||||||
|
`../../storage/assets`,
|
||||||
|
normalizePath(originalFilename)
|
||||||
|
);
|
||||||
const outputFilepath = process.env.STORAGE_DIR
|
const outputFilepath = process.env.STORAGE_DIR
|
||||||
? path.join(process.env.STORAGE_DIR, "assets", normalizePath(newFilename))
|
? path.join(process.env.STORAGE_DIR, "assets", normalizePath(newFilename))
|
||||||
: path.join(__dirname, `../../storage/assets`, normalizePath(newFilename));
|
: path.join(__dirname, `../../storage/assets`, normalizePath(newFilename));
|
||||||
|
|||||||
@ -61,6 +61,13 @@ function parseAuthHeader(headerValue = null, apiKey = null) {
|
|||||||
return { [headerValue]: apiKey };
|
return { [headerValue]: apiKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeJsonParse(jsonString, fallback = null) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonString);
|
||||||
|
} catch {}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
reqBody,
|
reqBody,
|
||||||
multiUserMode,
|
multiUserMode,
|
||||||
@ -69,4 +76,5 @@ module.exports = {
|
|||||||
decodeJWT,
|
decodeJWT,
|
||||||
userFromSession,
|
userFromSession,
|
||||||
parseAuthHeader,
|
parseAuthHeader,
|
||||||
|
safeJsonParse,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user