New user account validations ()

* force lowercase and no space for new and
 edit user modals

* edit account modal validations

* use pattern for form validation + remove validations from edit user

* revert comment deletions

* comment fix

* update validation message

* update regex
allow updating by block name changes to invalid names

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-08-07 11:35:37 -07:00 committed by GitHub
parent d072875e43
commit c97066526a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 56 additions and 9 deletions
frontend/src
components/UserMenu/AccountModal
pages/Admin/Users
NewUserModal
UserRow/EditUserModal
server/models

View file

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

View file

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

View file

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

View file

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