better password shit
This commit is contained in:
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user