diff --git a/frontend/src/components/UserMenu/AccountModal/index.jsx b/frontend/src/components/UserMenu/AccountModal/index.jsx index cb39fdd59045d0fb2ba57ecf64edbf8f1439d789..9fac4aaebb9477691bb46167be8a72731cc4a95e 100644 --- a/frontend/src/components/UserMenu/AccountModal/index.jsx +++ b/frontend/src/components/UserMenu/AccountModal/index.jsx @@ -7,6 +7,7 @@ import { Plus, X } from "@phosphor-icons/react"; export default function AccountModal({ user, hideModal }) { const { pfp, setPfp } = usePfp(); + const handleFileUpload = async (event) => { const file = event.target.files[0]; if (!file) return false; @@ -133,6 +134,10 @@ export default function AccountModal({ user, hideModal }) { required autoComplete="off" /> + <p className="mt-2 text-xs text-white/60"> + Username must be only contain lowercase letters, numbers, + underscores, and hyphens with no spaces + </p> </div> <div> <label @@ -143,10 +148,14 @@ export default function AccountModal({ user, hideModal }) { </label> <input name="password" - type="password" + type="text" className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder={`${user.username}'s new password`} + minLength={8} /> + <p className="mt-2 text-xs text-white/60"> + Password must be at least 8 characters long + </p> </div> <LanguagePreference /> </div> diff --git a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx index c784228b17fa9d8327107b57e50269b4a17e5918..3af7ebd4a26adcece544ed3696d961037227a021 100644 --- a/frontend/src/pages/Admin/Users/NewUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/NewUserModal/index.jsx @@ -7,6 +7,7 @@ import { RoleHintDisplay } from ".."; export default function NewUserModal({ closeModal }) { const [error, setError] = useState(null); const [role, setRole] = useState("default"); + const handleCreate = async (e) => { setError(null); e.preventDefault(); @@ -54,7 +55,18 @@ export default function NewUserModal({ closeModal }) { minLength={2} required={true} autoComplete="off" + pattern="^[a-z0-9_-]+$" + onInvalid={(e) => + e.target.setCustomValidity( + "Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces" + ) + } + onChange={(e) => e.target.setCustomValidity("")} /> + <p className="mt-2 text-xs text-white/60"> + Username must be only contain lowercase letters, numbers, + underscores, and hyphens with no spaces + </p> </div> <div> <label @@ -70,7 +82,11 @@ export default function NewUserModal({ closeModal }) { placeholder="User's initial password" required={true} autoComplete="off" + minLength={8} /> + <p className="mt-2 text-xs text-white/60"> + Password must be at least 8 characters long + </p> </div> <div> <label @@ -84,10 +100,10 @@ export default function NewUserModal({ closeModal }) { required={true} defaultValue={"default"} onChange={(e) => setRole(e.target.value)} - className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500" + className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500 w-full" > <option value="default">Default</option> - <option value="manager">Manager </option> + <option value="manager">Manager</option> {user?.role === "admin" && ( <option value="admin">Administrator</option> )} diff --git a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx index 313f81f23415070585b9b3ee797f7ecad5c9ae97..ec234c2f4130374adce2cc04f466b996bc489684 100644 --- a/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx +++ b/frontend/src/pages/Admin/Users/UserRow/EditUserModal/index.jsx @@ -52,11 +52,15 @@ export default function EditUserModal({ currentUser, user, closeModal }) { type="text" className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder="User's username" - minLength={2} defaultValue={user.username} + minLength={2} required={true} autoComplete="off" /> + <p className="mt-2 text-xs text-white/60"> + Username must be only contain lowercase letters, numbers, + underscores, and hyphens with no spaces + </p> </div> <div> <label @@ -71,7 +75,11 @@ export default function EditUserModal({ currentUser, user, closeModal }) { className="bg-zinc-900 placeholder:text-white/20 border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5" placeholder={`${user.username}'s new password`} autoComplete="off" + minLength={8} /> + <p className="mt-2 text-xs text-white/60"> + Password must be at least 8 characters long + </p> </div> <div> <label @@ -85,7 +93,7 @@ export default function EditUserModal({ currentUser, user, closeModal }) { required={true} defaultValue={user.role} onChange={(e) => setRole(e.target.value)} - className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500" + className="rounded-lg bg-zinc-900 px-4 py-2 text-sm text-white border-gray-500 focus:ring-blue-500 focus:border-blue-500 w-full" > <option value="default">Default</option> <option value="manager">Manager</option> diff --git a/server/models/user.js b/server/models/user.js index 4b14bb58f9e18428d51c5314f4db1ee96ef673a6..a149a45ea6ae5f4e3952a05b338e9d35a90b5e8c 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -2,6 +2,7 @@ const prisma = require("../utils/prisma"); const { EventLogs } = require("./eventLogs"); const User = { + usernameRegex: new RegExp(/^[a-z0-9_-]+$/), writable: [ // Used for generic updates so we can validate keys in request body "username", @@ -32,7 +33,6 @@ const User = { return String(role); }, }, - // validations for the above writable fields. castColumnValue: function (key, value) { switch (key) { @@ -55,6 +55,12 @@ const User = { } try { + // Do not allow new users to bypass validation + if (!this.usernameRegex.test(username)) + throw new Error( + "Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces" + ); + const bcrypt = require("bcrypt"); const hashedPassword = bcrypt.hashSync(password, 10); const user = await prisma.users.create({ @@ -70,7 +76,6 @@ const User = { return { user: null, error: error.message }; } }, - // Log the changes to a user object, but omit sensitive fields // that are not meant to be logged. loggedChanges: function (updates, prev = {}) { @@ -93,7 +98,6 @@ const User = { where: { id: parseInt(userId) }, }); if (!currentUser) return { success: false, error: "User not found" }; - // Removes non-writable fields for generic updates // and force-casts to the proper type; Object.entries(updates).forEach(([key, value]) => { @@ -123,6 +127,17 @@ const User = { updates.password = bcrypt.hashSync(updates.password, 10); } + if ( + updates.hasOwnProperty("username") && + currentUser.username !== updates.username && + !this.usernameRegex.test(updates.username) + ) + return { + success: false, + error: + "Username must be only contain lowercase letters, numbers, underscores, and hyphens with no spaces", + }; + const user = await prisma.users.update({ where: { id: parseInt(userId) }, data: updates, @@ -170,7 +185,6 @@ const User = { return null; } }, - // Returns user object with all fields _get: async function (clause = {}) { try {