better password shit

This commit is contained in:
KingKDot
2026-07-21 13:44:33 -05:00
parent d602de396e
commit ff337bc36b
13 changed files with 360 additions and 17 deletions
@@ -61,6 +61,17 @@ setInterval(() => {
}, 60_000);
function getPublicPasswordPolicy() {
const security = getConfig().security;
return {
minLength: Math.min(128, Math.max(6, Number(security.passwordMinLength) || 6)),
requireUppercase: Boolean(security.passwordRequireUppercase),
requireLowercase: Boolean(security.passwordRequireLowercase),
requireNumber: Boolean(security.passwordRequireNumber),
requireSymbol: Boolean(security.passwordRequireSymbol),
};
}
type RegistrationRouteDeps = {
requestIP?: (req: Request) => { address?: string } | null | undefined;
};
@@ -80,6 +91,7 @@ export async function handleRegistrationRoutes(
return Response.json({
enabled: mode !== "off",
mode: mode === "off" ? "off" : mode,
passwordPolicy: getPublicPasswordPolicy(),
});
}
@@ -0,0 +1,103 @@
import { afterEach, describe, expect, test } from "bun:test";
import { generateToken } from "../../auth";
import { createUser, deleteUser, getUserById } from "../../users";
import { handleUsersRoutes } from "./users-routes";
const PASSWORD = "Aa1!MustChangeRouteTestPass_2026";
const createdUserIds: number[] = [];
const mockServer = {
requestIP: () => ({ address: "127.0.0.1" }),
};
async function createAuthedAdmin() {
const username = `mcp_admin_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e6).toString(36)}`;
const result = await createUser(username, PASSWORD, "admin", "test");
expect(result.success).toBe(true);
expect(typeof result.userId).toBe("number");
createdUserIds.push(result.userId!);
const user = getUserById(result.userId!);
expect(user).not.toBeNull();
const token = await generateToken(user!);
return { user: user!, token };
}
afterEach(() => {
while (createdUserIds.length > 0) {
const id = createdUserIds.pop();
if (typeof id === "number") deleteUser(id);
}
});
describe("admin-created users must change password", () => {
test("POST /api/users marks new users for password change", async () => {
const admin = await createAuthedAdmin();
const username = `mcp_user_${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" }),
}),
url,
mockServer,
);
expect(res).not.toBeNull();
expect(res!.status).toBe(200);
const body = await res!.json();
expect(typeof body.userId).toBe("number");
createdUserIds.push(body.userId);
const created = getUserById(body.userId);
expect(created?.must_change_password).toBe(1);
});
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)}`;
const createUrl = new URL("https://localhost/api/users");
const createRes = await handleUsersRoutes(
new Request(createUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${admin.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password: PASSWORD, role: "operator" }),
}),
createUrl,
mockServer,
);
expect(createRes).not.toBeNull();
expect(createRes!.status).toBe(200);
const createBody = await createRes!.json();
createdUserIds.push(createBody.userId);
const created = getUserById(createBody.userId);
expect(created?.must_change_password).toBe(1);
expect(created).not.toBeNull();
const userToken = await generateToken(created!);
const updateUrl = new URL(`https://localhost/api/users/${createBody.userId}/password`);
const updateRes = await handleUsersRoutes(
new Request(updateUrl, {
method: "PUT",
headers: {
Authorization: `Bearer ${userToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ currentPassword: PASSWORD, newPassword: PASSWORD }),
}),
updateUrl,
mockServer,
);
expect(updateRes).not.toBeNull();
expect(updateRes!.status).toBe(200);
expect(getUserById(createBody.userId)?.must_change_password).toBe(0);
});
});
@@ -94,7 +94,7 @@ export async function handleUsersRoutes(
return Response.json({ error: "Invalid role" }, { status: 400 });
}
const result = await createUser(username, password, role, authedUser.username);
const result = await createUser(username, password, role, authedUser.username, true);
if (result.success) {
const ip = server.requestIP(req)?.address || "unknown";
+3 -2
View File
@@ -667,6 +667,7 @@ export async function createUser(
password: string,
role: UserRole,
createdBy: string,
mustChangePassword = false,
): Promise<{ success: boolean; error?: string; userId?: number }> {
if (!username || username.length < 3 || username.length > 32) {
return {
@@ -701,9 +702,9 @@ export async function createUser(
const result = db
.prepare(
"INSERT INTO users (username, password_hash, role, created_at, created_by, client_scope, plugin_scope, can_build, can_upload_files) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
"INSERT INTO users (username, password_hash, role, created_at, created_by, must_change_password, client_scope, plugin_scope, can_build, can_upload_files) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.run(username, passwordHash, role, Date.now(), createdBy, role === "admin" ? "all" : "none", role === "admin" ? "all" : "none", role === "admin" || role === "operator" ? 1 : 0, role === "admin" ? 1 : 0);
.run(username, passwordHash, role, Date.now(), createdBy, mustChangePassword ? 1 : 0, role === "admin" ? "all" : "none", role === "admin" ? "all" : "none", role === "admin" || role === "operator" ? 1 : 0, role === "admin" ? 1 : 0);
invalidateNotificationDeliveryCache();