From 1c473621aa7431778c1a472c038e97fa05a68639 Mon Sep 17 00:00:00 2001
From: Sean Hatfield <seanhatfield5@gmail.com>
Date: Mon, 17 Jun 2024 16:24:41 -0700
Subject: [PATCH] [FEAT] Add user management endpoints to list all users via
 developer API (#1708)

* add user management endpoints to list all users via developer api

* rename users to user management in swagger docs

* linting

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
---
 server/endpoints/api/index.js                |   2 +
 server/endpoints/api/userManagement/index.js |  64 +++++++++++
 server/swagger/init.js                       | 107 ++++++++++---------
 server/swagger/openapi.json                  |  56 ++++++++++
 4 files changed, 177 insertions(+), 52 deletions(-)
 create mode 100644 server/endpoints/api/userManagement/index.js

diff --git a/server/endpoints/api/index.js b/server/endpoints/api/index.js
index e5149ad70..c5a2b8a8d 100644
--- a/server/endpoints/api/index.js
+++ b/server/endpoints/api/index.js
@@ -4,6 +4,7 @@ const { apiAuthEndpoints } = require("./auth");
 const { apiDocumentEndpoints } = require("./document");
 const { apiSystemEndpoints } = require("./system");
 const { apiWorkspaceEndpoints } = require("./workspace");
+const { apiUserManagementEndpoints } = require("./userManagement");
 
 // All endpoints must be documented and pass through the validApiKey Middleware.
 // How to JSDoc an endpoint
@@ -16,6 +17,7 @@ function developerEndpoints(app, router) {
   apiSystemEndpoints(router);
   apiWorkspaceEndpoints(router);
   apiDocumentEndpoints(router);
+  apiUserManagementEndpoints(router);
 }
 
 module.exports = { developerEndpoints };
diff --git a/server/endpoints/api/userManagement/index.js b/server/endpoints/api/userManagement/index.js
new file mode 100644
index 000000000..656fc580b
--- /dev/null
+++ b/server/endpoints/api/userManagement/index.js
@@ -0,0 +1,64 @@
+const { User } = require("../../../models/user");
+const { multiUserMode } = require("../../../utils/http");
+const { validApiKey } = require("../../../utils/middleware/validApiKey");
+
+function apiUserManagementEndpoints(app) {
+  if (!app) return;
+
+  app.get("/v1/users", [validApiKey], async (request, response) => {
+    /*
+      #swagger.tags = ['User Management']
+      #swagger.description = 'List all users'
+      #swagger.responses[200] = {
+        content: {
+          "application/json": {
+            schema: {
+              type: 'object',
+              example: {
+                users: [
+                  {
+                    "id": 1,
+                    "username": "john_doe",
+                    "role": "admin"
+                  },
+                  {
+                    "id": 2,
+                    "username": "jane_smith",
+                    "role": "default"
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    #swagger.responses[403] = {
+      schema: {
+        "$ref": "#/definitions/InvalidAPIKey"
+      }
+    }
+     #swagger.responses[401] = {
+      description: "Instance is not in Multi-User mode. Permission denied.",
+    }
+      */
+    try {
+      if (!multiUserMode(response))
+        return response
+          .status(401)
+          .send("Instance is not in Multi-User mode. Permission denied.");
+
+      const users = await User.where();
+      const filteredUsers = users.map((user) => ({
+        id: user.id,
+        username: user.username,
+        role: user.role,
+      }));
+      response.status(200).json({ users: filteredUsers });
+    } catch (e) {
+      console.log(e.message, e);
+      response.sendStatus(500).end();
+    }
+  });
+}
+
+module.exports = { apiUserManagementEndpoints };
diff --git a/server/swagger/init.js b/server/swagger/init.js
index 4707b5823..064814565 100644
--- a/server/swagger/init.js
+++ b/server/swagger/init.js
@@ -1,73 +1,76 @@
-const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
-const fs = require('fs')
-const path = require('path')
+const swaggerAutogen = require("swagger-autogen")({ openapi: "3.0.0" });
+const fs = require("fs");
+const path = require("path");
 
 const doc = {
   info: {
-    version: '1.0.0',
-    title: 'AnythingLLM Developer API',
-    description: 'API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.',
+    version: "1.0.0",
+    title: "AnythingLLM Developer API",
+    description:
+      "API endpoints that enable programmatic reading, writing, and updating of your AnythingLLM instance. UI supplied by Swagger.io.",
   },
   // Swagger-autogen does not allow us to use relative paths as these will resolve to
   // http:///api in the openapi.json file, so we need to monkey-patch this post-generation.
-  host: '/api',
-  schemes: ['http'],
+  host: "/api",
+  schemes: ["http"],
   securityDefinitions: {
     BearerAuth: {
-      type: 'http',
-      scheme: 'bearer',
-      bearerFormat: 'JWT'
-    }
+      type: "http",
+      scheme: "bearer",
+      bearerFormat: "JWT",
+    },
   },
-  security: [
-    { BearerAuth: [] }
-  ],
+  security: [{ BearerAuth: [] }],
   definitions: {
     InvalidAPIKey: {
-      message: 'Invalid API Key',
+      message: "Invalid API Key",
     },
-  }
+  },
 };
 
-const outputFile = path.resolve(__dirname, './openapi.json');
+const outputFile = path.resolve(__dirname, "./openapi.json");
 const endpointsFiles = [
-  '../endpoints/api/auth/index.js',
-  '../endpoints/api/admin/index.js',
-  '../endpoints/api/document/index.js',
-  '../endpoints/api/workspace/index.js',
-  '../endpoints/api/system/index.js',
+  "../endpoints/api/auth/index.js",
+  "../endpoints/api/admin/index.js",
+  "../endpoints/api/document/index.js",
+  "../endpoints/api/workspace/index.js",
+  "../endpoints/api/system/index.js",
+  "../endpoints/api/userManagement/index.js",
 ];
 
-swaggerAutogen(outputFile, endpointsFiles, doc)
-  .then(({ data }) => {
-
-    // Remove Authorization parameters from arguments.
-    for (const path of Object.keys(data.paths)) {
-      if (data.paths[path].hasOwnProperty('get')) {
-        let parameters = data.paths[path].get?.parameters || [];
-        parameters = parameters.filter((arg) => arg.name !== 'Authorization');
-        data.paths[path].get.parameters = parameters;
-      }
-
-      if (data.paths[path].hasOwnProperty('post')) {
-        let parameters = data.paths[path].post?.parameters || [];
-        parameters = parameters.filter((arg) => arg.name !== 'Authorization');
-        data.paths[path].post.parameters = parameters;
-      }
+swaggerAutogen(outputFile, endpointsFiles, doc).then(({ data }) => {
+  // Remove Authorization parameters from arguments.
+  for (const path of Object.keys(data.paths)) {
+    if (data.paths[path].hasOwnProperty("get")) {
+      let parameters = data.paths[path].get?.parameters || [];
+      parameters = parameters.filter((arg) => arg.name !== "Authorization");
+      data.paths[path].get.parameters = parameters;
+    }
 
-      if (data.paths[path].hasOwnProperty('delete')) {
-        let parameters = data.paths[path].delete?.parameters || [];
-        parameters = parameters.filter((arg) => arg.name !== 'Authorization');
-        data.paths[path].delete.parameters = parameters;
-      }
+    if (data.paths[path].hasOwnProperty("post")) {
+      let parameters = data.paths[path].post?.parameters || [];
+      parameters = parameters.filter((arg) => arg.name !== "Authorization");
+      data.paths[path].post.parameters = parameters;
     }
 
-    const openApiSpec = {
-      ...data,
-      servers: [{
-        url: "/api"
-      }]
+    if (data.paths[path].hasOwnProperty("delete")) {
+      let parameters = data.paths[path].delete?.parameters || [];
+      parameters = parameters.filter((arg) => arg.name !== "Authorization");
+      data.paths[path].delete.parameters = parameters;
     }
-    fs.writeFileSync(outputFile, JSON.stringify(openApiSpec, null, 2), { encoding: 'utf-8', flag: 'w' });
-    console.log(`Swagger-autogen:  \x1b[32mPatched servers.url ✔\x1b[0m`)
-  })
\ No newline at end of file
+  }
+
+  const openApiSpec = {
+    ...data,
+    servers: [
+      {
+        url: "/api",
+      },
+    ],
+  };
+  fs.writeFileSync(outputFile, JSON.stringify(openApiSpec, null, 2), {
+    encoding: "utf-8",
+    flag: "w",
+  });
+  console.log(`Swagger-autogen:  \x1b[32mPatched servers.url ✔\x1b[0m`);
+});
diff --git a/server/swagger/openapi.json b/server/swagger/openapi.json
index 2154c5611..2a1b55437 100644
--- a/server/swagger/openapi.json
+++ b/server/swagger/openapi.json
@@ -2370,6 +2370,62 @@
           }
         }
       }
+    },
+    "/v1/users": {
+      "get": {
+        "tags": [
+          "User Management"
+        ],
+        "description": "List all users",
+        "parameters": [],
+        "responses": {
+          "200": {
+            "description": "OK",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "example": {
+                    "users": [
+                      {
+                        "id": 1,
+                        "username": "john_doe",
+                        "role": "admin"
+                      },
+                      {
+                        "id": 2,
+                        "username": "jane_smith",
+                        "role": "default"
+                      }
+                    ]
+                  }
+                }
+              }
+            }
+          },
+          "401": {
+            "description": "Instance is not in Multi-User mode. Permission denied."
+          },
+          "403": {
+            "description": "Forbidden",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidAPIKey"
+                }
+              },
+              "application/xml": {
+                "schema": {
+                  "$ref": "#/components/schemas/InvalidAPIKey"
+                }
+              }
+            }
+          },
+          "500": {
+            "description": "Internal Server Error"
+          }
+        }
+      }
     }
   },
   "components": {
-- 
GitLab