From 6dd1fdc546fa521618a8875f9cb7fbf5db7fcc97 Mon Sep 17 00:00:00 2001 From: Sean Hatfield <seanhatfield5@gmail.com> Date: Thu, 27 Feb 2025 07:23:24 +0800 Subject: [PATCH] Add bio field to user (#3346) * add bio to users table * lint * add bio field to edit user admin page * fix bio saving on new user * simplify updating localstorage user * linting --------- Co-authored-by: timothycarambat <rambat1010@gmail.com> --- .../UserMenu/AccountModal/index.jsx | 16 +++++++++- .../pages/Admin/Users/NewUserModal/index.jsx | 15 ++++++++++ .../Users/UserRow/EditUserModal/index.jsx | 29 ++++++++++++++++++- server/endpoints/system.js | 10 +++---- server/models/user.js | 9 ++++++ .../20250226005538_init/migration.sql | 2 ++ server/prisma/schema.prisma | 1 + 7 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 server/prisma/migrations/20250226005538_init/migration.sql diff --git a/frontend/src/components/UserMenu/AccountModal/index.jsx b/frontend/src/components/UserMenu/AccountModal/index.jsx index 9de868934..a77c51457 100644 --- a/frontend/src/components/UserMenu/AccountModal/index.jsx +++ b/frontend/src/components/UserMenu/AccountModal/index.jsx @@ -50,9 +50,9 @@ export default function AccountModal({ user, hideModal }) { const { success, error } = await System.updateUser(data); if (success) { let storedUser = JSON.parse(localStorage.getItem(AUTH_USER)); - if (storedUser) { storedUser.username = data.username; + storedUser.bio = data.bio; localStorage.setItem(AUTH_USER, JSON.stringify(storedUser)); } showToast("Profile updated.", "success", { clear: true }); @@ -164,6 +164,20 @@ export default function AccountModal({ user, hideModal }) { Password must be at least 8 characters long </p> </div> + <div> + <label + htmlFor="bio" + className="block mb-2 text-sm font-medium text-white" + > + Bio + </label> + <textarea + name="bio" + className="border-none bg-theme-settings-input-bg placeholder:text-theme-settings-input-placeholder border-gray-500 text-white text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5 min-h-[100px] resize-y" + placeholder="Tell us about yourself..." + defaultValue={user.bio} + /> + </div> <div className="flex flex-row gap-x-8"> <ThemePreference /> <LanguagePreference /> diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index 54dab87ee..8b11c1a11 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -95,6 +95,21 @@ export default function NewUserModal({ closeModal }) { Password must be at least 8 characters long </p> </div> + <div> + <label + htmlFor="bio" + className="block mb-2 text-sm font-medium text-white" + > + Bio + </label> + <textarea + name="bio" + className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5" + placeholder="User's bio" + autoComplete="off" + rows={3} + /> + </div> <div> <label htmlFor="role" diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index 8908b17e6..5a88e4d89 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { X } from "@phosphor-icons/react"; import Admin from "@/models/admin"; import { MessageLimitInput, RoleHintDisplay } from "../.."; +import { AUTH_USER } from "@/utils/constants"; export default function EditUserModal({ currentUser, user, closeModal }) { const [role, setRole] = useState(user.role); @@ -27,7 +28,17 @@ export default function EditUserModal({ currentUser, user, closeModal }) { } const { success, error } = await Admin.updateUser(user.id, data); - if (success) window.location.reload(); + if (success) { + // Update local storage if we're editing our own user + if (currentUser && currentUser.id === user.id) { + currentUser.username = data.username; + currentUser.bio = data.bio; + currentUser.role = data.role; + localStorage.setItem(AUTH_USER, JSON.stringify(currentUser)); + } + + window.location.reload(); + } setError(error); }; @@ -92,6 +103,22 @@ export default function EditUserModal({ currentUser, user, closeModal }) { Password must be at least 8 characters long </p> </div> + <div> + <label + htmlFor="bio" + className="block mb-2 text-sm font-medium text-white" + > + Bio + </label> + <textarea + name="bio" + className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5" + placeholder="User's bio" + defaultValue={user.bio} + autoComplete="off" + rows={3} + /> + </div> <div> <label htmlFor="role" diff --git a/server/endpoints/system.js b/server/endpoints/system.js index a11edec62..8d1877269 100644 --- a/server/endpoints/system.js +++ b/server/endpoints/system.js @@ -1089,7 +1089,7 @@ function systemEndpoints(app) { app.post("/system/user", [validatedRequest], async (request, response) => { try { const sessionUser = await userFromSession(request, response); - const { username, password } = reqBody(request); + const { username, password, bio } = reqBody(request); const id = Number(sessionUser.id); if (!id) { @@ -1098,12 +1098,10 @@ function systemEndpoints(app) { } const updates = {}; - if (username) { + if (username) updates.username = User.validations.username(String(username)); - } - if (password) { - updates.password = String(password); - } + if (password) updates.password = String(password); + if (bio) updates.bio = String(bio); if (Object.keys(updates).length === 0) { response diff --git a/server/models/user.js b/server/models/user.js index e6915d9dc..5979de347 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -22,6 +22,7 @@ const User = { "role", "suspended", "dailyMessageLimit", + "bio", ], validations: { username: (newValue = "") => { @@ -54,6 +55,12 @@ const User = { } return limit; }, + bio: (bio = "") => { + if (!bio || typeof bio !== "string") return ""; + if (bio.length > 1000) + throw new Error("Bio cannot be longer than 1,000 characters"); + return String(bio); + }, }, // validations for the above writable fields. castColumnValue: function (key, value) { @@ -77,6 +84,7 @@ const User = { password, role = "default", dailyMessageLimit = null, + bio = "", }) { const passwordCheck = this.checkPasswordComplexity(password); if (!passwordCheck.checkedOK) { @@ -97,6 +105,7 @@ const User = { username: this.validations.username(username), password: hashedPassword, role: this.validations.role(role), + bio: this.validations.bio(bio), dailyMessageLimit: this.validations.dailyMessageLimit(dailyMessageLimit), }, diff --git a/server/prisma/migrations/20250226005538_init/migration.sql b/server/prisma/migrations/20250226005538_init/migration.sql new file mode 100644 index 000000000..4902fbc68 --- /dev/null +++ b/server/prisma/migrations/20250226005538_init/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "users" ADD COLUMN "bio" TEXT DEFAULT ''; diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 37c82d4dd..8372d33a4 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -68,6 +68,7 @@ model users { createdAt DateTime @default(now()) lastUpdatedAt DateTime @default(now()) dailyMessageLimit Int? + bio String? @default("") workspace_chats workspace_chats[] workspace_users workspace_users[] embed_configs embed_configs[] -- GitLab