diff --git a/frontend/src/models/admin.js b/frontend/src/models/admin.js
index 0798a3e7..df27ac02 100644
--- a/frontend/src/models/admin.js
+++ b/frontend/src/models/admin.js
@@ -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) => {
diff --git a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx
index 3aef87a6..e69da4ae 100644
--- a/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx
+++ b/frontend/src/pages/Admin/Invitations/NewInviteModal/index.jsx
@@ -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 (
@@ -61,11 +87,45 @@ export default function NewInviteModal({ closeModal }) {
)}
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{" "}
+ default role and automatically be added to workspaces
+ selected.
+
+ {workspaces.length > 0 && !invite && (
+
+
+
+
+ Auto-add invitee to workspaces
+
+
+ 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.
+
+
+
+
+ {workspaces.map((workspace) => (
+
+ ))}
+
+
+
+ )}
+
{!invite ? (
<>
@@ -99,3 +159,31 @@ export default function NewInviteModal({ closeModal }) {
);
}
+
+function WorkspaceOption({ workspace, selected, toggleSelection }) {
+ return (
+ 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`}
+ >
+
+
+
+ {workspace.name}
+
+
+ );
+}
diff --git a/server/endpoints/admin.js b/server/endpoints/admin.js
index 792cf2dd..f55cbb6e 100644
--- a/server/endpoints/admin.js
+++ b/server/endpoints/admin.js
@@ -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",
{
diff --git a/server/endpoints/api/admin/index.js b/server/endpoints/api/admin/index.js
index e91672e0..228777ab 100644
--- a/server/endpoints/api/admin/index.js
+++ b/server/endpoints/api/admin/index.js
@@ -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);
diff --git a/server/models/invite.js b/server/models/invite.js
index ff9ae868..781a9434 100644
--- a/server/models/invite.js
+++ b/server/models/invite.js
@@ -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);
diff --git a/server/prisma/migrations/20240326231053_init/migration.sql b/server/prisma/migrations/20240326231053_init/migration.sql
new file mode 100644
index 00000000..85fe8be7
--- /dev/null
+++ b/server/prisma/migrations/20240326231053_init/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "invites" ADD COLUMN "workspaceIds" TEXT;
diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma
index e6121e29..fbb5f61d 100644
--- a/server/prisma/schema.prisma
+++ b/server/prisma/schema.prisma
@@ -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[]
diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json
index 77dc974a..e0ee35a5 100644
--- a/server/swagger/openapi.json
+++ b/server/swagger/openapi.json
@@ -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
+ ]
+ }
+ }
+ }
}
}
},
diff --git a/server/utils/files/logo.js b/server/utils/files/logo.js
index 68108401..68c56c21 100644
--- a/server/utils/files/logo.js
+++ b/server/utils/files/logo.js
@@ -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));
diff --git a/server/utils/http/index.js b/server/utils/http/index.js
index 83e3fa5d..084b09c7 100644
--- a/server/utils/http/index.js
+++ b/server/utils/http/index.js
@@ -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,
};