Skip to main content

Update User

updateUser edits the scalar (pure) fields of an existing user โ€” name, email, position, isActive, features, and optionally the password (which is re-hashed). It deliberately does not touch relations; those are handled by the sibling act updateUserRelations. It belongs to the user model.

The act lives in src/user/updateUser/.

The validator (updateUser.val.ts)โ€‹

set spreads activeRoleMixin, requires _id, and makes every editable field optional โ€” you send only what changed. features is optional (no defaulted, so passing nothing leaves the existing array untouched). Note there's no avatar / organizations / units here: those are relations and belong to the other act. get is selectStruct("user", 1).

import {
array,
boolean,
object,
objectIdValidation,
optional,
string,
} from "lesan";
import { selectStruct } from "../../../mod.ts";
import { activeRoleMixin } from "@lib";
import { feature_enums } from "@model";

export const updateUserValidator = () => {
return object({
set: object({
...activeRoleMixin,
_id: objectIdValidation,
first_name: optional(string()),
last_name: optional(string()),
email: optional(string()),
password: optional(string()),
position: optional(string()),
isActive: optional(boolean()),
features: optional(array(object({ feature: feature_enums }))),
}),
get: selectStruct("user", 1),
});
};
note

Runtime-agnostic imports "lesan" is this repo's path alias for the framework source. On npm/Bun you'd import from @hemedani/lesan, and on Deno from jsr:@hemedani/lesan. The @model / @lib / ../../../mod.ts aliases stay as they are in your project (see Project Layout).

The registration (mod.ts)โ€‹

Like addUser, this act uses validationRunType: "create" and a Manager/Admin-only grantAccess. Unit-level roles (OrgHead, UnitHead, ...) cannot edit users.

import { grantAccess, setTokens, setUser } from "@lib";
import { coreApp } from "../../../mod.ts";
import { updateUserFn } from "./updateUser.fn.ts";
import { updateUserValidator } from "./updateUser.val.ts";

export const updateUserSetup = () =>
coreApp.acts.setAct({
schema: "user",
actName: "updateUser",
validationRunType: "create",
preAct: [setTokens, setUser, grantAccess([{ roles: ["Manager", "Admin"] }])],
validator: updateUserValidator(),
fn: updateUserFn,
});

The implementation (updateUser.fn.ts)โ€‹

Destructure _id and password out of set; everything else lands in rest. Build an update object with { $set: { ...rest } }. If a new password was supplied, merge a hashed copy into $set (so the stored password is always hashed, never plaintext). Then user.findOneAndUpdate({ filter, update, projection }) returns the freshly-updated, projected document.

import { type ActFn, ObjectId } from "lesan";
import { user } from "../../../mod.ts";
import { hashPassword } from "@lib";
import { throwError } from "@lib";

export const updateUserFn: ActFn = async (body) => {
const {
set: { _id, password, ...rest },
get,
} = body.details;

const userId = new ObjectId(_id as string);

const update = { $set: { ...rest } } as Record<string, unknown>;
if (password) {
update.$set = { ...(update.$set as Record<string, unknown>), password: await hashPassword(password as string) };
}

const updated = await user.findOneAndUpdate({
filter: { _id: userId },
update,
projection: get,
});

!updated && throwError("user not found");
return updated;
};

A note on the $set rebuild: instead of mutating rest in place, it spreads update.$set and adds password โ€” keeping rest free of the raw password, so even the intermediate object never holds a plaintext secret.

In the workflowโ€‹

updateUser is the edit-half of the CRUD pair with addUser. Pair it with updateUserRelations when the request mixes scalar edits and relation changes.

Run itโ€‹

Needs token, activeRoleId, and the target user's _id. Send only the fields you want to change:

curl -X POST http://localhost:1380/lesan \
-H "Content-Type: application/json" \
-H "token: <jwt>" \
-d '{
"model": "user",
"act": "updateUser",
"details": {
"set": {
"activeRoleId": "<roleId>",
"_id": "<userId>",
"position": "Senior Purchasing Manager",
"isActive": true
},
"get": {
"_id": 1,
"first_name": 1,
"last_name": 1,
"position": 1,
"isActive": 1
}
}
}'

Errors & fixesโ€‹

MessageSourceWhat it meansHow to fix
you should send your id with token key in req headersetTokensMissing token header.Add -H "token: <jwt>".
Invalid or expired tokensetTokensJWT didn't verify.Re-login.
Invalid or missing token datasetUserToken payload had no _id.Re-login.
user not existsetUserThe token's user was deleted.Use a different account.
activeRoleId is requiredgrantAccessset.activeRoleId was omitted.Always send activeRoleId in set.
Active role not foundgrantAccessThe activeRoleId doesn't match any role on the user.Use a roleId from the login response.
You cant do thisgrantAccessActive role is not Manager or Admin.Use a manager/admin role, or the ghost superuser.
user not foundupdateUserFnNo user matches the _id.Check the _id (24-char hex ObjectId); verify the user exists.
Duplicate key error (framework error)MongoDBSetting email to one that's already taken.The email unique index rejected it โ€” pick another email.
Generic validation errorsuperstructMissing _id, or an invalid field type (e.g. isActive not boolean, features not { feature: <enum> }).Keep _id as a 24-char hex string and the rest within their declared superstruct types.