diff --git a/Overlord-Server/public/assets/users.js b/Overlord-Server/public/assets/users.js
index cf0ea16..1959e4c 100644
--- a/Overlord-Server/public/assets/users.js
+++ b/Overlord-Server/public/assets/users.js
@@ -16,6 +16,7 @@ const errorMessage = document.getElementById("error-message");
const errorText = document.getElementById("error-text");
const generatePasswordBtn = document.getElementById("generate-password-btn");
const generatedPasswordHint = document.getElementById("generated-password-hint");
+const mustChangePasswordInput = document.getElementById("must-change-password");
const logoutBtn = document.getElementById("logout-btn");
const currentUserEl = document.getElementById("username-display");
const currentRoleEl = document.getElementById("role-badge");
@@ -441,12 +442,13 @@ userForm.addEventListener("submit", async (e) => {
const username = formData.get("username");
const password = formData.get("password");
const role = formData.get("role");
+ const mustChangePassword = mustChangePasswordInput?.checked !== false;
try {
const res = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ username, password, role }),
+ body: JSON.stringify({ username, password, role, mustChangePassword }),
});
const data = await res.json();
diff --git a/Overlord-Server/public/users.html b/Overlord-Server/public/users.html
index e069858..ce7c40a 100644
--- a/Overlord-Server/public/users.html
+++ b/Overlord-Server/public/users.html
@@ -208,6 +208,20 @@
+
+
{
expect(created?.must_change_password).toBe(1);
});
+ test("POST /api/users allows an explicit first-login prompt exemption", async () => {
+ const admin = await createAuthedAdmin();
+ const username = `mcp_exempt_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
+ const url = new URL("https://localhost/api/users");
+
+ const res = await handleUsersRoutes(
+ new Request(url, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${admin.token}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ username,
+ password: PASSWORD,
+ role: "viewer",
+ mustChangePassword: false,
+ }),
+ }),
+ url,
+ mockServer,
+ );
+
+ expect(res).not.toBeNull();
+ expect(res!.status).toBe(200);
+ const body = await res!.json();
+ createdUserIds.push(body.userId);
+ expect(getUserById(body.userId)?.must_change_password).toBe(0);
+ });
+
test("self password update may reuse the initial password and clears the prompt", async () => {
const admin = await createAuthedAdmin();
const username = `mcp_same_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
diff --git a/Overlord-Server/src/server/routes/users-routes.ts b/Overlord-Server/src/server/routes/users-routes.ts
index 9240a4e..9665230 100644
--- a/Overlord-Server/src/server/routes/users-routes.ts
+++ b/Overlord-Server/src/server/routes/users-routes.ts
@@ -84,7 +84,7 @@ export async function handleUsersRoutes(
if (req.method === "POST" && url.pathname === "/api/users") {
const authedUser = requirePermission(user, "users:manage");
const body = await req.json();
- const { username, password, role } = body;
+ const { username, password, role, mustChangePassword } = body;
if (!username || !password || !role) {
return Response.json({ error: "Missing required fields" }, { status: 400 });
@@ -94,7 +94,13 @@ export async function handleUsersRoutes(
return Response.json({ error: "Invalid role" }, { status: 400 });
}
- const result = await createUser(username, password, role, authedUser.username, true);
+ const result = await createUser(
+ username,
+ password,
+ role,
+ authedUser.username,
+ mustChangePassword !== false,
+ );
if (result.success) {
const ip = server.requestIP(req)?.address || "unknown";
diff --git a/Overlord-Server/test/e2e/access.e2e.ts b/Overlord-Server/test/e2e/access.e2e.ts
index bb3bdfd..880e5d4 100644
--- a/Overlord-Server/test/e2e/access.e2e.ts
+++ b/Overlord-Server/test/e2e/access.e2e.ts
@@ -83,6 +83,7 @@ test("admin can generate passwords and inspect combined permission sources", asy
await page.goto("/users");
await page.locator("#add-user-btn").click();
+ await expect(page.locator("#must-change-password")).toBeChecked();
await page.locator("#generate-password-btn").click();
const generatedPassword = await page.locator("#password").inputValue();
expect(generatedPassword.length).toBeGreaterThanOrEqual(20);
diff --git a/Overlord-Server/test/e2e/global-setup.ts b/Overlord-Server/test/e2e/global-setup.ts
index a58e453..0e49574 100644
--- a/Overlord-Server/test/e2e/global-setup.ts
+++ b/Overlord-Server/test/e2e/global-setup.ts
@@ -51,7 +51,7 @@ async function seedBrowserUsers(): Promise {
for (const account of [ONBOARDING_USER, OPERATOR, VIEWER]) {
const response = await authenticatedRequest("/api/users", adminCookie, {
method: "POST",
- body: JSON.stringify(account),
+ body: JSON.stringify({ ...account, mustChangePassword: false }),
});
if (!response.ok) {
throw new Error(`Could not seed ${account.username}: ${response.status}`);