feat(auth): add password reset functionality and user model update
- Introduced a `mustResetPassword` field in the User model to manage password reset requirements. - Enhanced authentication flow to support password changes, including validation and error handling. - Updated frontend components to handle password reset scenarios and integrate with the new API endpoints. - Modified authentication context and hooks to accommodate the new password reset logic. - Adjusted E2E tests to ensure proper coverage for the password reset functionality.
This commit is contained in:
@@ -109,7 +109,7 @@ export default defineConfig({
|
||||
{
|
||||
command: "cd ../backend && npx prisma db push && npx ts-node src/index.ts",
|
||||
url: `${BACKEND_URL}/health`,
|
||||
reuseExistingServer: true,
|
||||
reuseExistingServer: false,
|
||||
timeout: 120000,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
@@ -131,7 +131,7 @@ export default defineConfig({
|
||||
{
|
||||
command: "cd ../frontend && npm run dev -- --host",
|
||||
url: FRONTEND_URL,
|
||||
reuseExistingServer: true,
|
||||
reuseExistingServer: false,
|
||||
timeout: 120000,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
|
||||
@@ -7,6 +7,7 @@ type AuthStatus = {
|
||||
enabled: boolean;
|
||||
authenticated: boolean;
|
||||
bootstrapRequired?: boolean;
|
||||
user?: { mustResetPassword?: boolean } | null;
|
||||
};
|
||||
|
||||
const AUTH_USERNAME = process.env.AUTH_USERNAME || "admin";
|
||||
@@ -43,10 +44,44 @@ const fetchAuthStatus = async (request: APIRequestContext): Promise<AuthStatus>
|
||||
|
||||
export const ensureApiAuthenticated = async (request: APIRequestContext) => {
|
||||
const status = await fetchAuthStatus(request);
|
||||
if (!status.enabled || status.authenticated) {
|
||||
if (!status.enabled || (status.authenticated && !status.user?.mustResetPassword)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resetIfRequired = async () => {
|
||||
if (!status.user?.mustResetPassword) return;
|
||||
|
||||
let resetResponse = await request.post(`${BASE_URL}/auth/password`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
currentPassword: AUTH_PASSWORD,
|
||||
newPassword: AUTH_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
if (!resetResponse.ok() && resetResponse.status() === 403) {
|
||||
await refreshCsrfToken(request);
|
||||
resetResponse = await request.post(`${BASE_URL}/auth/password`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
currentPassword: AUTH_PASSWORD,
|
||||
newPassword: AUTH_PASSWORD,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!resetResponse.ok()) {
|
||||
const text = await resetResponse.text();
|
||||
throw new Error(`Failed to reset admin password: ${resetResponse.status()} ${text}`);
|
||||
}
|
||||
};
|
||||
|
||||
if (status.bootstrapRequired) {
|
||||
let response = await request.post(`${BASE_URL}/auth/bootstrap`, {
|
||||
headers: {
|
||||
@@ -78,6 +113,7 @@ export const ensureApiAuthenticated = async (request: APIRequestContext) => {
|
||||
throw new Error(`Failed to bootstrap test session: ${response.status()} ${text}`);
|
||||
}
|
||||
|
||||
await resetIfRequired();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { PrismaClient } from "../../backend/src/generated/client";
|
||||
import { test, expect } from "./fixtures";
|
||||
|
||||
const AUTH_USERNAME = process.env.AUTH_USERNAME || "admin";
|
||||
const AUTH_PASSWORD = process.env.AUTH_PASSWORD || "admin123";
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
|
||||
const ensureLoggedOut = async (page: Page) => {
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/");
|
||||
const signInPrompt = page.getByText("Sign in to access your drawings");
|
||||
if (await signInPrompt.isVisible().catch(() => false)) {
|
||||
return;
|
||||
}
|
||||
await page.goto("/settings");
|
||||
const logoutButton = page.getByRole("button", { name: /Log out/i });
|
||||
if (await logoutButton.isVisible().catch(() => false)) {
|
||||
await logoutButton.click();
|
||||
}
|
||||
await expect(signInPrompt).toBeVisible();
|
||||
};
|
||||
|
||||
const login = async (page: Page, password: string) => {
|
||||
await page.getByLabel("Username or Email").fill(AUTH_USERNAME);
|
||||
await page.getByLabel("Password").fill(password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
};
|
||||
|
||||
const waitForResetOrDashboard = async (page: Page) => {
|
||||
const resetPrompt = page.getByText("Reset the admin password");
|
||||
const dashboardReady = page.getByPlaceholder("Search drawings...");
|
||||
const settingsHeader = page.getByRole("heading", { name: "Settings" });
|
||||
|
||||
await Promise.race([
|
||||
resetPrompt.waitFor({ state: "visible", timeout: 15000 }).catch(() => null),
|
||||
dashboardReady.waitFor({ state: "visible", timeout: 15000 }).catch(() => null),
|
||||
settingsHeader.waitFor({ state: "visible", timeout: 15000 }).catch(() => null),
|
||||
]);
|
||||
|
||||
if (await resetPrompt.isVisible().catch(() => false)) {
|
||||
return "reset" as const;
|
||||
}
|
||||
|
||||
if (await dashboardReady.isVisible().catch(() => false)) {
|
||||
return "dashboard" as const;
|
||||
}
|
||||
|
||||
if (await settingsHeader.isVisible().catch(() => false)) {
|
||||
return "settings" as const;
|
||||
}
|
||||
|
||||
return "unknown" as const;
|
||||
};
|
||||
|
||||
const ensureDashboard = async (page: Page) => {
|
||||
await expect(page.getByPlaceholder("Search drawings...")).toBeVisible({ timeout: 30000 });
|
||||
};
|
||||
|
||||
const setMustResetPassword = async (enabled: boolean) => {
|
||||
if (!DATABASE_URL) {
|
||||
throw new Error("DATABASE_URL is not set for e2e test.");
|
||||
}
|
||||
|
||||
const prisma = new PrismaClient({
|
||||
datasources: {
|
||||
db: { url: DATABASE_URL },
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const admin = await prisma.user.findFirst({
|
||||
where: { username: AUTH_USERNAME },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!admin) {
|
||||
throw new Error(`Admin user ${AUTH_USERNAME} not found.`);
|
||||
}
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: admin.id },
|
||||
data: { mustResetPassword: enabled },
|
||||
});
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
test.describe("Admin password reset", () => {
|
||||
test.use({ skipAuth: true });
|
||||
|
||||
test("prompts and clears reset requirement for generated admin password", async ({ page }) => {
|
||||
await ensureLoggedOut(page);
|
||||
|
||||
await login(page, AUTH_PASSWORD);
|
||||
let initialState = await waitForResetOrDashboard(page);
|
||||
if (initialState === "settings") {
|
||||
await page.goto("/");
|
||||
initialState = await waitForResetOrDashboard(page);
|
||||
}
|
||||
if (initialState === "reset") {
|
||||
await page.getByLabel("Current Password").fill(AUTH_PASSWORD);
|
||||
await page.getByLabel("New Password").fill(AUTH_PASSWORD);
|
||||
await page.getByLabel("Confirm Password").fill(AUTH_PASSWORD);
|
||||
await page.getByRole("button", { name: "Reset password" }).click();
|
||||
await expect(page.getByPlaceholder("Search drawings...")).toBeVisible({ timeout: 30000 });
|
||||
}
|
||||
|
||||
await setMustResetPassword(true);
|
||||
await ensureLoggedOut(page);
|
||||
|
||||
await login(page, AUTH_PASSWORD);
|
||||
await expect(page.getByText("Reset the admin password")).toBeVisible({ timeout: 30000 });
|
||||
await page.getByLabel("Current Password").fill(AUTH_PASSWORD);
|
||||
await page.getByLabel("New Password").fill(AUTH_PASSWORD);
|
||||
await page.getByLabel("Confirm Password").fill(AUTH_PASSWORD);
|
||||
await page.getByRole("button", { name: "Reset password" }).click();
|
||||
|
||||
await page.goto("/");
|
||||
await ensureDashboard(page);
|
||||
|
||||
await ensureLoggedOut(page);
|
||||
await login(page, AUTH_PASSWORD);
|
||||
await ensureDashboard(page);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user