Update admin theme/layout and refresh README details.

Align the project baseline with the latest admin interface styling and layout structure while documenting setup and usage updates in README.
This commit is contained in:
vlad
2026-07-14 17:12:28 +03:00
commit 86cc3fa541
278 changed files with 19416 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
import { expect, test } from "@playwright/test";
import { API_URL, adminLogin, loginViaUi, registerVerifyLogin, uniqueEmail } from "../helpers/api";
test.describe("§15.7 scenarios 5 & 9: Admin users", () => {
test("admin blocks user → blocked user cannot login", async ({ page, request }) => {
const email = uniqueEmail("e2e-block");
const session = await registerVerifyLogin(request, email);
const admin = await adminLogin(request);
const adminToken = (await admin.json()).access_token;
const patch = await request.patch(`${API_URL}/api/v1/admin/users/${session.user.id}`, {
headers: { Authorization: `Bearer ${adminToken}` },
data: { status: "blocked" }
});
expect(patch.ok()).toBeTruthy();
await loginViaUi(page, email, "Valid1234");
await expect(page).toHaveURL(/\/login$/);
});
test("admin can open admin users page", async ({ page }) => {
await loginViaUi(page, "admin@compton.example", "Admin1234");
await expect(page).toHaveURL(/\/admin$/);
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
});
test("admin session survives reload on /admin", async ({ page }) => {
await loginViaUi(page, "admin@compton.example", "Admin1234");
await expect(page).toHaveURL(/\/admin$/);
await page.goto("/admin");
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
});
test("non-admin cannot open admin page", async ({ page, request }) => {
const email = uniqueEmail("e2e-nonadmin");
await registerVerifyLogin(request, email);
await loginViaUi(page, email, "Valid1234");
await expect(page).toHaveURL(/\/profile$/);
await page.goto("/admin");
await expect(page).toHaveURL(/\/$/);
});
test("admin cannot block self", async ({ request }) => {
const admin = await adminLogin(request);
const body = await admin.json();
const response = await request.patch(`${API_URL}/api/v1/admin/users/${body.user.id}`, {
headers: { Authorization: `Bearer ${body.access_token}` },
data: { status: "blocked" }
});
expect(response.status()).toBe(400);
expect((await response.json()).detail).toBe("SELF_BLOCK_FORBIDDEN");
});
test("admin cannot demote self", async ({ request }) => {
const admin = await adminLogin(request);
const body = await admin.json();
const response = await request.patch(`${API_URL}/api/v1/admin/users/${body.user.id}`, {
headers: { Authorization: `Bearer ${body.access_token}` },
data: { role: "user" }
});
expect(response.status()).toBe(400);
expect((await response.json()).detail).toBe("SELF_DEMOTION_FORBIDDEN");
});
});
+6
View File
@@ -0,0 +1,6 @@
import { expect, test } from "@playwright/test";
test("login page renders", async ({ page }) => {
await page.goto("/login");
await expect(page.getByRole("heading", { name: "Вход" })).toBeVisible();
});
+33
View File
@@ -0,0 +1,33 @@
import { expect, test } from "@playwright/test";
import { API_URL, fetchLatestToken, registerVerifyLogin, uniqueEmail } from "../helpers/api";
test.describe("§15.7 scenario 3: Password reset", () => {
test("forgot password → reset → login with new password", async ({ request }) => {
const email = uniqueEmail("e2e-reset");
const oldPassword = "Valid1234";
const newPassword = "ResetValid1";
await registerVerifyLogin(request, email, oldPassword);
const forgot = await request.post(`${API_URL}/api/v1/auth/forgot-password`, {
data: { email }
});
expect(forgot.ok()).toBeTruthy();
const token = await fetchLatestToken(request, email, "reset_password");
const reset = await request.post(`${API_URL}/api/v1/auth/reset-password`, {
data: { token, new_password: newPassword }
});
expect(reset.ok()).toBeTruthy();
const oldLogin = await request.post(`${API_URL}/api/v1/auth/login`, {
data: { email, password: oldPassword }
});
expect(oldLogin.status()).toBe(401);
const newLogin = await request.post(`${API_URL}/api/v1/auth/login`, {
data: { email, password: newPassword }
});
expect(newLogin.ok()).toBeTruthy();
});
});
@@ -0,0 +1,37 @@
import { expect, test } from "@playwright/test";
import { API_URL, registerVerifyLogin, uniqueEmail } from "../helpers/api";
function extractRefreshCookie(headers: Record<string, string>): string {
const setCookie = headers["set-cookie"] ?? headers["Set-Cookie"] ?? "";
const match = setCookie.match(/refresh_token=([^;]+)/);
if (!match) {
throw new Error("refresh_token cookie missing");
}
return match[1];
}
test.describe("§15.7 scenario 7: Refresh rotation", () => {
test("old refresh token rejected after rotation; reuse revokes family", async ({ request }) => {
const email = uniqueEmail("e2e-refresh");
await registerVerifyLogin(request, email);
const login = await request.post(`${API_URL}/api/v1/auth/login`, { data: { email, password: "Valid1234" } });
const oldRefresh = extractRefreshCookie(login.headers());
const rotated = await request.post(`${API_URL}/api/v1/auth/refresh`, {
headers: { Cookie: `refresh_token=${oldRefresh}` }
});
expect(rotated.ok()).toBeTruthy();
const newRefresh = extractRefreshCookie(rotated.headers());
const oldReuse = await request.post(`${API_URL}/api/v1/auth/refresh`, {
headers: { Cookie: `refresh_token=${oldRefresh}` }
});
expect(oldReuse.status()).toBe(401);
const familyReuse = await request.post(`${API_URL}/api/v1/auth/refresh`, {
headers: { Cookie: `refresh_token=${newRefresh}` }
});
expect(familyReuse.status()).toBe(401);
});
});
@@ -0,0 +1,40 @@
import { expect, test } from "@playwright/test";
import {
loginViaUi,
registerUser,
uniqueEmail,
verifyEmail
} from "../helpers/api";
test.describe("§15.7 scenario 2: Auth full journey", () => {
test("register → verify → login → profile edit → change password → logout", async ({
page,
request
}) => {
const email = uniqueEmail("e2e-flow");
const password = "Valid1234";
const newPassword = "NewValid1";
await registerUser(request, email, password);
await verifyEmail(request, email);
await loginViaUi(page, email, password);
await expect(page).toHaveURL(/\/profile$/);
await expect(page.getByText(email)).toBeVisible();
await page.getByLabel("Display name").fill("E2E User");
await page.getByRole("button", { name: "Save name" }).click();
await expect(page.getByText("Profile updated")).toBeVisible();
await page.getByPlaceholder("Current password").fill(password);
await page.getByPlaceholder("New password").fill(newPassword);
await page.getByRole("button", { name: "Change password" }).click();
await expect(page.getByText("Password changed")).toBeVisible();
await page.getByRole("button", { name: "Logout" }).click();
await expect(page).toHaveURL(/\/login$/);
await loginViaUi(page, email, newPassword);
await expect(page).toHaveURL(/\/profile$/);
});
});
@@ -0,0 +1,25 @@
import { expect, test } from "@playwright/test";
import { API_URL, adminLogin } from "../helpers/api";
test.describe("§15.7 scenario 4: Admin content publish", () => {
test("admin creates content → publish → visible on /pages/:slug", async ({ page, request }) => {
const admin = await adminLogin(request);
const headers = { Authorization: `Bearer ${(await admin.json()).access_token}` };
const slug = `e2e-page-${Date.now()}`;
const created = await request.post(`${API_URL}/api/v1/content/pages`, {
headers,
data: {
slug,
title: "E2E Published Page",
body: "<p>Published by admin</p>",
status: "published"
}
});
expect(created.ok()).toBeTruthy();
await page.goto(`/pages/${slug}`);
await expect(page.getByRole("heading", { name: "E2E Published Page" })).toBeVisible();
await expect(page.getByText("Published by admin")).toBeVisible();
});
});
+64
View File
@@ -0,0 +1,64 @@
import { APIRequestContext } from "@playwright/test";
export const API_URL = process.env.E2E_API_URL ?? "http://127.0.0.1:8001";
export function uniqueEmail(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
}
export async function registerUser(request: APIRequestContext, email: string, password = "Valid1234") {
const response = await request.post(`${API_URL}/api/v1/auth/register`, {
data: { email, password }
});
return response;
}
export async function fetchLatestToken(
request: APIRequestContext,
email: string,
template: "verify_email" | "reset_password"
): Promise<string> {
const response = await request.get(`${API_URL}/api/v1/test/emails/latest-token`, {
params: { to: email, template }
});
if (!response.ok()) {
throw new Error(`Token not found for ${email} (${template})`);
}
const body = await response.json();
return body.token as string;
}
export async function verifyEmail(request: APIRequestContext, email: string) {
const token = await fetchLatestToken(request, email, "verify_email");
return request.post(`${API_URL}/api/v1/auth/verify-email`, { data: { token } });
}
export async function loginApi(request: APIRequestContext, email: string, password = "Valid1234") {
return request.post(`${API_URL}/api/v1/auth/login`, { data: { email, password } });
}
export async function adminLogin(request: APIRequestContext) {
return loginApi(request, "admin@compton.example", "Admin1234");
}
export async function registerVerifyLogin(
request: APIRequestContext,
email: string,
password = "Valid1234"
) {
await registerUser(request, email, password);
await verifyEmail(request, email);
const login = await loginApi(request, email, password);
const body = await login.json();
return {
accessToken: body.access_token as string,
user: body.user as { id: string; email: string; role: string; status: string }
};
}
export async function loginViaUi(page: import("@playwright/test").Page, email: string, password: string) {
await page.goto("/login");
await page.getByPlaceholder("Email").fill(email);
await page.getByPlaceholder("Password").fill(password);
await page.getByRole("button", { name: "Login" }).click();
}
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
test.describe("§15.7 scenario 1: Landing", () => {
test("hero and marquee are visible", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("heading", { name: /Технологии будущего на вашей ферме/i })).toBeVisible();
await expect(page.locator("#integrations .marquee-track")).toBeVisible();
});
test("respects prefers-reduced-motion", async ({ page }) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/");
await expect(page.getByRole("heading", { name: /Технологии будущего на вашей ферме/i })).toBeVisible();
});
});
@@ -0,0 +1,52 @@
import { expect, test } from "@playwright/test";
import { API_URL, registerVerifyLogin, uniqueEmail } from "../helpers/api";
const tinyPng = Buffer.from(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
"base64"
);
test.describe("§15.7 scenario 10: Avatar validation", () => {
test("rejects svg, oversize and accepts valid png", async ({ request }) => {
const session = await registerVerifyLogin(request, uniqueEmail("e2e-avatar"));
const headers = { Authorization: `Bearer ${session.accessToken}` };
const svg = await request.post(`${API_URL}/api/v1/users/me/avatar`, {
headers,
multipart: {
file: {
name: "avatar.svg",
mimeType: "image/svg+xml",
buffer: Buffer.from("<svg xmlns='http://www.w3.org/2000/svg'></svg>")
}
}
});
expect(svg.status()).toBe(400);
const oversize = await request.post(`${API_URL}/api/v1/users/me/avatar`, {
headers,
multipart: {
file: {
name: "big.png",
mimeType: "image/png",
buffer: Buffer.concat([tinyPng, Buffer.alloc(2 * 1024 * 1024 + 1)])
}
}
});
expect(oversize.status()).toBe(400);
expect((await oversize.json()).detail).toBe("FILE_TOO_LARGE");
const valid = await request.post(`${API_URL}/api/v1/users/me/avatar`, {
headers,
multipart: {
file: {
name: "avatar.png",
mimeType: "image/png",
buffer: tinyPng
}
}
});
expect(valid.ok()).toBeTruthy();
expect((await valid.json()).profile.avatar_url).toContain("/api/v1/media/files/avatars/");
});
});
+18
View File
@@ -0,0 +1,18 @@
import { expect, test } from "@playwright/test";
import { registerUser, uniqueEmail } from "../helpers/api";
test.describe("§15.7 scenario 6: Pending user", () => {
test("pending user cannot access /profile", async ({ page, request }) => {
const email = uniqueEmail("e2e-pending");
await registerUser(request, email, "Valid1234");
await page.goto("/profile");
await expect(page).toHaveURL(/\/login$/);
await page.goto("/login");
await page.getByPlaceholder("Email").fill(email);
await page.getByPlaceholder("Password").fill("Valid1234");
await page.getByRole("button", { name: "Login" }).click();
await expect(page).toHaveURL(/\/login$/);
});
});
+15
View File
@@ -0,0 +1,15 @@
import { expect, test } from "@playwright/test";
import { API_URL, registerVerifyLogin, uniqueEmail } from "../helpers/api";
test.describe("§15.7 scenario 8: IDOR", () => {
test("user A cannot patch user B via admin route", async ({ request }) => {
const userA = await registerVerifyLogin(request, uniqueEmail("e2e-a"));
const userB = await registerVerifyLogin(request, uniqueEmail("e2e-b"));
const forbidden = await request.patch(`${API_URL}/api/v1/admin/users/${userB.user.id}`, {
headers: { Authorization: `Bearer ${userA.accessToken}` },
data: { status: "blocked" }
});
expect(forbidden.status()).toBe(403);
});
});