Add ability to add invitee to workspaces automatically ()

This commit is contained in:
Timothy Carambat 2024-03-26 16:38:32 -07:00 committed by GitHub
parent 1cd255c1ec
commit 1ecefe8bed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 182 additions and 13 deletions
frontend/src
models
pages/Admin/Invitations/NewInviteModal
server
endpoints
models
prisma
migrations/20240326231053_init
schema.prisma
swagger
utils

View file

@ -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) => {

View file

@ -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>
);
}

View file

@ -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",
{

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "invites" ADD COLUMN "workspaceIds" TEXT;

View file

@ -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[]

View file

@ -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
]
}
}
}
}
}
},

View file

@ -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));

View file

@ -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,
};