fix tests
This commit is contained in:
@@ -16,6 +16,7 @@ const errorMessage = document.getElementById("error-message");
|
|||||||
const errorText = document.getElementById("error-text");
|
const errorText = document.getElementById("error-text");
|
||||||
const generatePasswordBtn = document.getElementById("generate-password-btn");
|
const generatePasswordBtn = document.getElementById("generate-password-btn");
|
||||||
const generatedPasswordHint = document.getElementById("generated-password-hint");
|
const generatedPasswordHint = document.getElementById("generated-password-hint");
|
||||||
|
const mustChangePasswordInput = document.getElementById("must-change-password");
|
||||||
const logoutBtn = document.getElementById("logout-btn");
|
const logoutBtn = document.getElementById("logout-btn");
|
||||||
const currentUserEl = document.getElementById("username-display");
|
const currentUserEl = document.getElementById("username-display");
|
||||||
const currentRoleEl = document.getElementById("role-badge");
|
const currentRoleEl = document.getElementById("role-badge");
|
||||||
@@ -441,12 +442,13 @@ userForm.addEventListener("submit", async (e) => {
|
|||||||
const username = formData.get("username");
|
const username = formData.get("username");
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
const role = formData.get("role");
|
const role = formData.get("role");
|
||||||
|
const mustChangePassword = mustChangePasswordInput?.checked !== false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/users", {
|
const res = await fetch("/api/users", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ username, password, role }),
|
body: JSON.stringify({ username, password, role, mustChangePassword }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|||||||
@@ -208,6 +208,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label class="flex items-start gap-3 rounded-lg border border-slate-700 bg-slate-800/50 p-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
id="must-change-password"
|
||||||
|
name="mustChangePassword"
|
||||||
|
type="checkbox"
|
||||||
|
checked
|
||||||
|
class="mt-0.5 h-4 w-4 accent-violet-500"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span class="block text-sm font-medium text-slate-200">Prompt for password confirmation on first login</span>
|
||||||
|
<span class="block text-xs text-slate-500 mt-0.5">The user may keep this password, but must enter it again before accessing the dashboard.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id="error-message"
|
id="error-message"
|
||||||
class="hidden p-3 bg-red-900/20 border border-red-800 rounded-lg text-red-400 text-sm"
|
class="hidden p-3 bg-red-900/20 border border-red-800 rounded-lg text-red-400 text-sm"
|
||||||
|
|||||||
@@ -57,6 +57,36 @@ describe("admin-created users must change password", () => {
|
|||||||
expect(created?.must_change_password).toBe(1);
|
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 () => {
|
test("self password update may reuse the initial password and clears the prompt", async () => {
|
||||||
const admin = await createAuthedAdmin();
|
const admin = await createAuthedAdmin();
|
||||||
const username = `mcp_same_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
|
const username = `mcp_same_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export async function handleUsersRoutes(
|
|||||||
if (req.method === "POST" && url.pathname === "/api/users") {
|
if (req.method === "POST" && url.pathname === "/api/users") {
|
||||||
const authedUser = requirePermission(user, "users:manage");
|
const authedUser = requirePermission(user, "users:manage");
|
||||||
const body = await req.json();
|
const body = await req.json();
|
||||||
const { username, password, role } = body;
|
const { username, password, role, mustChangePassword } = body;
|
||||||
|
|
||||||
if (!username || !password || !role) {
|
if (!username || !password || !role) {
|
||||||
return Response.json({ error: "Missing required fields" }, { status: 400 });
|
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 });
|
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) {
|
if (result.success) {
|
||||||
const ip = server.requestIP(req)?.address || "unknown";
|
const ip = server.requestIP(req)?.address || "unknown";
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ test("admin can generate passwords and inspect combined permission sources", asy
|
|||||||
await page.goto("/users");
|
await page.goto("/users");
|
||||||
|
|
||||||
await page.locator("#add-user-btn").click();
|
await page.locator("#add-user-btn").click();
|
||||||
|
await expect(page.locator("#must-change-password")).toBeChecked();
|
||||||
await page.locator("#generate-password-btn").click();
|
await page.locator("#generate-password-btn").click();
|
||||||
const generatedPassword = await page.locator("#password").inputValue();
|
const generatedPassword = await page.locator("#password").inputValue();
|
||||||
expect(generatedPassword.length).toBeGreaterThanOrEqual(20);
|
expect(generatedPassword.length).toBeGreaterThanOrEqual(20);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ async function seedBrowserUsers(): Promise<void> {
|
|||||||
for (const account of [ONBOARDING_USER, OPERATOR, VIEWER]) {
|
for (const account of [ONBOARDING_USER, OPERATOR, VIEWER]) {
|
||||||
const response = await authenticatedRequest("/api/users", adminCookie, {
|
const response = await authenticatedRequest("/api/users", adminCookie, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(account),
|
body: JSON.stringify({ ...account, mustChangePassword: false }),
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Could not seed ${account.username}: ${response.status}`);
|
throw new Error(`Could not seed ${account.username}: ${response.status}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user