feat(auth): enhance authentication system with multi-user support and admin role management
- Implemented multi-user authentication with role-based access control. - Added environment variables for initial admin user setup. - Updated README and example environment file with new authentication options. - Introduced user and system configuration models in the database schema. - Enhanced authentication middleware to support user registration and role management. - Updated frontend to handle new authentication flows, including admin user creation and role updates.
This commit is contained in:
+146
-24
@@ -1,34 +1,44 @@
|
||||
import { APIRequestContext, Page } from "@playwright/test";
|
||||
import { API_URL, getCsrfHeaders } from "./api";
|
||||
import { API_URL, getCsrfHeaders, refreshCsrfToken } from "./api";
|
||||
|
||||
const BASE_URL = process.env.API_URL || API_URL;
|
||||
|
||||
type AuthStatus = {
|
||||
enabled: boolean;
|
||||
authenticated: boolean;
|
||||
bootstrapRequired?: boolean;
|
||||
};
|
||||
|
||||
const AUTH_USERNAME = process.env.AUTH_USERNAME || "admin";
|
||||
const AUTH_PASSWORD = process.env.AUTH_PASSWORD || "admin";
|
||||
|
||||
const authStatusCache = new WeakMap<APIRequestContext, AuthStatus>();
|
||||
const AUTH_PASSWORD = process.env.AUTH_PASSWORD || "admin123";
|
||||
|
||||
const fetchAuthStatus = async (request: APIRequestContext): Promise<AuthStatus> => {
|
||||
const cached = authStatusCache.get(request);
|
||||
// Only use cache if we're already authenticated
|
||||
if (cached?.authenticated) return cached;
|
||||
const maxAttempts = 3;
|
||||
|
||||
const response = await request.get(`${API_URL}/auth/status`);
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to fetch auth status: ${response.status()} ${text}`);
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const response = await request.get(`${BASE_URL}/auth/status`);
|
||||
if (response.ok()) {
|
||||
return (await response.json()) as AuthStatus;
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (response.status() === 429 && attempt < maxAttempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 + attempt * 500));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch auth status: ${response.status()} ${text}`);
|
||||
} catch (error) {
|
||||
if (attempt < maxAttempts - 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 + attempt * 250));
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const data = (await response.json()) as AuthStatus;
|
||||
authStatusCache.set(request, data);
|
||||
return data;
|
||||
};
|
||||
|
||||
const setAuthStatus = (request: APIRequestContext, status: AuthStatus) => {
|
||||
authStatusCache.set(request, status);
|
||||
throw new Error("Failed to fetch auth status");
|
||||
};
|
||||
|
||||
export const ensureApiAuthenticated = async (request: APIRequestContext) => {
|
||||
@@ -37,11 +47,44 @@ export const ensureApiAuthenticated = async (request: APIRequestContext) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const headers = await getCsrfHeaders(request);
|
||||
const response = await request.post(`${API_URL}/auth/login`, {
|
||||
if (status.bootstrapRequired) {
|
||||
let response = await request.post(`${BASE_URL}/auth/bootstrap`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
username: AUTH_USERNAME,
|
||||
password: AUTH_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok() && response.status() === 403) {
|
||||
await refreshCsrfToken(request);
|
||||
response = await request.post(`${BASE_URL}/auth/bootstrap`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
username: AUTH_USERNAME,
|
||||
password: AUTH_PASSWORD,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to bootstrap test session: ${response.status()} ${text}`);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let response = await request.post(`${BASE_URL}/auth/login`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
...(await getCsrfHeaders(request)),
|
||||
},
|
||||
data: {
|
||||
username: AUTH_USERNAME,
|
||||
@@ -49,14 +92,93 @@ export const ensureApiAuthenticated = async (request: APIRequestContext) => {
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok() && response.status() === 403) {
|
||||
await refreshCsrfToken(request);
|
||||
const freshHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
...(await getCsrfHeaders(request)),
|
||||
};
|
||||
response = await request.post(`${BASE_URL}/auth/login`, {
|
||||
headers: freshHeaders,
|
||||
data: {
|
||||
username: AUTH_USERNAME,
|
||||
password: AUTH_PASSWORD,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok()) {
|
||||
const text = await response.text();
|
||||
throw new Error(`Failed to authenticate test session: ${response.status()} ${text}`);
|
||||
}
|
||||
|
||||
setAuthStatus(request, { enabled: true, authenticated: true });
|
||||
};
|
||||
|
||||
export const ensurePageAuthenticated = async (page: Page) => {
|
||||
type EnsureAuthOptions = {
|
||||
skipNavigation?: boolean;
|
||||
};
|
||||
|
||||
export const ensurePageAuthenticated = async (
|
||||
page: Page,
|
||||
{ skipNavigation = false }: EnsureAuthOptions = {}
|
||||
) => {
|
||||
await ensureApiAuthenticated(page.request);
|
||||
const storageState = await page.request.storageState();
|
||||
if (storageState.cookies.length > 0) {
|
||||
await page.context().addCookies(
|
||||
storageState.cookies.filter((cookie) => cookie.name && cookie.value)
|
||||
);
|
||||
}
|
||||
|
||||
if (!skipNavigation) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
}
|
||||
|
||||
const dashboardReady = page.getByPlaceholder("Search drawings...");
|
||||
const identifierField = page.getByLabel("Username or Email");
|
||||
const passwordField = page.getByLabel("Password");
|
||||
|
||||
if (skipNavigation) {
|
||||
if (await identifierField.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||
await identifierField.fill(AUTH_USERNAME);
|
||||
await passwordField.fill(AUTH_PASSWORD);
|
||||
|
||||
const confirmPasswordField = page.getByLabel("Confirm Password");
|
||||
if (await confirmPasswordField.isVisible().catch(() => false)) {
|
||||
await confirmPasswordField.fill(AUTH_PASSWORD);
|
||||
}
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /sign in|create admin|create account/i })
|
||||
.click();
|
||||
await dashboardReady.waitFor({ state: "visible", timeout: 30000 });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
dashboardReady.waitFor({ state: "visible", timeout: 15000 }),
|
||||
identifierField.waitFor({ state: "visible", timeout: 15000 }),
|
||||
]);
|
||||
|
||||
if (await dashboardReady.isVisible().catch(() => false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await identifierField.isVisible().catch(() => false)) {
|
||||
await identifierField.fill(AUTH_USERNAME);
|
||||
await passwordField.fill(AUTH_PASSWORD);
|
||||
|
||||
const confirmPasswordField = page.getByLabel("Confirm Password");
|
||||
if (await confirmPasswordField.isVisible().catch(() => false)) {
|
||||
await confirmPasswordField.fill(AUTH_PASSWORD);
|
||||
}
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /sign in|create admin|create account/i })
|
||||
.click();
|
||||
await dashboardReady.waitFor({ state: "visible", timeout: 30000 });
|
||||
return;
|
||||
}
|
||||
|
||||
await dashboardReady.waitFor({ state: "visible", timeout: 15000 });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user