TASKS-1823 WebManager module
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# Access Control Module
|
||||
|
||||
The Access Control module manages admin accounts, roles, and permissions in the admin app. It is the UI boundary for deciding who can access which admin features.
|
||||
|
||||
## Source Map
|
||||
|
||||
| Area | Path |
|
||||
| ------------------------- | ------------------------------------------------------- |
|
||||
| Frontend module | `apps/admin/src/core/AccessControl/` |
|
||||
| Frontend routes | `apps/admin/src/core/AccessControl/config/routing.tsx` |
|
||||
| Frontend translations | `apps/admin/src/core/AccessControl/config/translation/` |
|
||||
| Backend admin module | `apps/backend/app/base_modules/Admin/` |
|
||||
| Backend role module | `apps/backend/app/base_modules/Role/` |
|
||||
| Backend permission module | `apps/backend/app/base_modules/Permission/` |
|
||||
| Permission loader | `apps/backend/app/services/permission_service.ts` |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| [Frontend](./frontend.md) | Routes, UI structure, data layer, forms, translations, and cache invalidation. |
|
||||
| [Backend](./backend.md) | Admin, role, and permission API routes, policies, validators, and permission loading. |
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- List, search, sort, paginate, create, edit, and delete admin accounts.
|
||||
- Assign roles to admin accounts.
|
||||
- List, create, edit, and delete roles.
|
||||
- Assign permissions to roles.
|
||||
- Load available permission definitions from backend module permission configs.
|
||||
- Gate access to screens and actions through the Permission core module.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- AccessControl owns management UI for admins, roles, and role permissions.
|
||||
- Backend Admin owns account persistence and profile update endpoint.
|
||||
- Backend Role owns role persistence and role-permission sync.
|
||||
- Backend Permission owns the available permission list endpoint.
|
||||
- Permission core module owns permission checking and React gates.
|
||||
- Auth core module owns the current authenticated admin and session state.
|
||||
|
||||
## Common Change Guide
|
||||
|
||||
| Change | Start Here |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| Add an admin field | `entities/AdminEntity.ts`, `data/api/dto.ts`, `data/mappers/AdminMapper.ts`, `view/forms/AdminForm/` |
|
||||
| Change admin list columns | `view/widgets/AdminTable.tsx` |
|
||||
| Change role form behavior | `view/forms/RoleForm/` |
|
||||
| Add a new permission source | Backend module `config/permissions.ts` and `apps/backend/app/services/permission_service.ts` |
|
||||
| Change cache invalidation | `data/queries/cache.ts` |
|
||||
| Change translations | `config/translation/en.json` and `config/translation/uk.json` |
|
||||
| Change backend admin API | `apps/backend/app/base_modules/Admin/controller/admin_controller.ts` |
|
||||
| Change backend role API | `apps/backend/app/base_modules/Role/controller/role_controller.ts` |
|
||||
@@ -0,0 +1,89 @@
|
||||
# Access Control Backend
|
||||
|
||||
Access Control frontend uses three backend base modules:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/
|
||||
apps/backend/app/base_modules/Role/
|
||||
apps/backend/app/base_modules/Permission/
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
| Method | Route | Backend | Purpose |
|
||||
| -------- | ----------------------- | ---------- | ------------------------------------------- |
|
||||
| `GET` | `/admin` | Admin | Paginated admin list. |
|
||||
| `GET` | `/admin/:id` | Admin | Admin details with roles. |
|
||||
| `POST` | `/admin` | Admin | Create admin and sync roles. |
|
||||
| `PUT` | `/admin/:id` | Admin | Update admin and sync roles. |
|
||||
| `DELETE` | `/admin/:id` | Admin | Delete admin. |
|
||||
| `PUT` | `/admin/update-profile` | Admin | Update current authenticated admin profile. |
|
||||
| `GET` | `/role` | Role | Paginated role list. |
|
||||
| `GET` | `/role/:id` | Role | Role details with permissions. |
|
||||
| `POST` | `/role` | Role | Create role and role-permission rows. |
|
||||
| `PUT` | `/role/:id` | Role | Update role and role-permission rows. |
|
||||
| `DELETE` | `/role/:id` | Role | Delete role. |
|
||||
| `GET` | `/permission` | Permission | List all available permission definitions. |
|
||||
|
||||
All routes use `middleware.auth()` except login routes owned by Auth.
|
||||
|
||||
## Authorization
|
||||
|
||||
Policies:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/policy/admin_policy.ts
|
||||
apps/backend/app/base_modules/Role/policy/role_policy.ts
|
||||
apps/backend/app/base_modules/Permission/policy/permission_policy.ts
|
||||
```
|
||||
|
||||
Permission slugs are defined in:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/config/permissions.ts
|
||||
apps/backend/app/base_modules/Role/config/permissions.ts
|
||||
```
|
||||
|
||||
Current AccessControl slugs:
|
||||
|
||||
```txt
|
||||
admins.view
|
||||
admins.create
|
||||
admins.update
|
||||
admins.delete
|
||||
role.view
|
||||
role.create
|
||||
role.update
|
||||
role.delete
|
||||
```
|
||||
|
||||
## Permission Loading
|
||||
|
||||
Available permissions are collected by:
|
||||
|
||||
```txt
|
||||
apps/backend/app/services/permission_service.ts
|
||||
```
|
||||
|
||||
`getAllPermissions()` scans `app/base_modules/*/config/permissions.ts` and `app/modules/*/config/permissions.ts`. Add permissions to the owning backend module config; do not hard-code them in the frontend.
|
||||
|
||||
## Validation
|
||||
|
||||
Validators:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/validators/admin_create_validator.ts
|
||||
apps/backend/app/base_modules/Admin/validators/admin_update_validator.ts
|
||||
apps/backend/app/base_modules/Admin/validators/admin_update_profile_validator.ts
|
||||
apps/backend/app/base_modules/Role/validator/role_create_validator.ts
|
||||
apps/backend/app/base_modules/Role/validator/role_update_validator.ts
|
||||
```
|
||||
|
||||
Keep frontend form rules compatible with backend validators. Backend validation is authoritative.
|
||||
|
||||
## Data Notes
|
||||
|
||||
- Admin create/update syncs assigned roles through the admin-role relation.
|
||||
- Role create/update rewrites `role_permissions` rows for the role.
|
||||
- Permission records returned by `/permission` are loaded from permission config files, not only database rows.
|
||||
- Deleting the final admin is blocked by the Admin controller.
|
||||
@@ -0,0 +1,141 @@
|
||||
# Access Control Frontend
|
||||
|
||||
The frontend module provides the admin-facing UI for managing admins, roles, and permissions.
|
||||
|
||||
Source: `apps/admin/src/core/AccessControl/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
AccessControl/
|
||||
config/ Routes, menu, messages, translations.
|
||||
data/
|
||||
api/ Raw HTTP calls and DTOs.
|
||||
mappers/ DTO-to-entity mapping.
|
||||
queries/ React Query hooks, keys, and cache invalidation.
|
||||
entities/ Admin, role, permission entities and enums.
|
||||
pages/ Route-level pages.
|
||||
view/
|
||||
forms/ Admin and role forms.
|
||||
layouts/ Module layout.
|
||||
widgets/ Tables and modal triggers.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Frontend route config:
|
||||
|
||||
```txt
|
||||
config/routing.tsx
|
||||
```
|
||||
|
||||
Mounted module path:
|
||||
|
||||
```txt
|
||||
/access-control
|
||||
```
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| ----------------------- | --------------- | ----------------------------------------- |
|
||||
| `/access-control/admin` | `AdminListPage` | Manage admin accounts and assigned roles. |
|
||||
| `/access-control/role` | `RoleListPage` | Manage roles and assigned permissions. |
|
||||
|
||||
## Data Layer
|
||||
|
||||
Raw API calls live in:
|
||||
|
||||
```txt
|
||||
data/api/AdminAPI.ts
|
||||
data/api/RoleAPI.ts
|
||||
data/api/PermissionAPI.ts
|
||||
data/api/dto.ts
|
||||
```
|
||||
|
||||
Query hooks live in:
|
||||
|
||||
```txt
|
||||
data/queries/admin.ts
|
||||
data/queries/role.ts
|
||||
data/queries/permission.ts
|
||||
```
|
||||
|
||||
Cache invalidation helpers live in:
|
||||
|
||||
```txt
|
||||
data/queries/cache.ts
|
||||
```
|
||||
|
||||
Current helpers:
|
||||
|
||||
```ts
|
||||
invalidateAdminList();
|
||||
invalidateAdminDetailed(id?);
|
||||
invalidateRoleList();
|
||||
invalidateRoleDetailed(id?);
|
||||
invalidatePermissionList();
|
||||
```
|
||||
|
||||
## DTO Mapping
|
||||
|
||||
The API layer returns DTOs. Query hooks map DTOs into entities before data reaches UI code.
|
||||
|
||||
```txt
|
||||
data/mappers/AdminMapper.ts
|
||||
data/mappers/RoleMapper.ts
|
||||
data/mappers/PermissionMapper.ts
|
||||
entities/AdminEntity.ts
|
||||
entities/RoleEntity.ts
|
||||
entities/PermissionEntity.ts
|
||||
```
|
||||
|
||||
Keep backend response shape in DTOs. Keep UI/domain shape in entities.
|
||||
|
||||
## Forms
|
||||
|
||||
Admin form:
|
||||
|
||||
```txt
|
||||
view/forms/AdminForm/
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Create and update admin data.
|
||||
- Select admin status.
|
||||
- Assign roles.
|
||||
- Optionally update password.
|
||||
|
||||
Role form:
|
||||
|
||||
```txt
|
||||
view/forms/RoleForm/
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Create and update role name.
|
||||
- Group available permissions by module slug prefix.
|
||||
- Assign permission slugs to the role.
|
||||
|
||||
## Widgets
|
||||
|
||||
| Widget | Purpose |
|
||||
| -------------------- | -------------------------------------------- |
|
||||
| `AdminTable` | Admin list, filters, actions, create button. |
|
||||
| `RoleTable` | Role list, actions, create button. |
|
||||
| `ManageAdminTrigger` | Admin create/edit dialog orchestration. |
|
||||
| `ManageRoleTrigger` | Role create/edit dialog orchestration. |
|
||||
| `DeleteAdminTrigger` | Admin delete confirmation and mutation. |
|
||||
| `DeleteRoleTrigger` | Role delete confirmation and mutation. |
|
||||
|
||||
## Translations
|
||||
|
||||
Translations are module-local:
|
||||
|
||||
```txt
|
||||
config/messages.ts
|
||||
config/translation/en.json
|
||||
config/translation/uk.json
|
||||
```
|
||||
|
||||
Use only `ACCESS_CONTROL.*` keys inside this module.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Account Settings Module
|
||||
|
||||
The Account Settings module provides the current admin user's profile settings screen. It lets the signed-in admin update profile fields and password, and view assigned roles.
|
||||
|
||||
## Source Map
|
||||
|
||||
| Area | Path |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| Frontend module | `apps/admin/src/core/AccountSettings/` |
|
||||
| Frontend routes | `apps/admin/src/core/AccountSettings/config/routing.tsx` |
|
||||
| Frontend translations | `apps/admin/src/core/AccountSettings/config/translation/` |
|
||||
| Auth provider dependency | `apps/admin/src/core/Auth/providers/AuthProvider/` |
|
||||
| Backend profile endpoint | `apps/backend/app/base_modules/Admin/controller/admin_controller.ts` |
|
||||
| Backend profile validator | `apps/backend/app/base_modules/Admin/validators/admin_update_profile_validator.ts` |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------- | --------------------------------------------------------- |
|
||||
| [Frontend](./frontend.md) | Route, page, form, profile update flow, and translations. |
|
||||
| [Backend](./backend.md) | Current-user profile endpoint and validation. |
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- Display current authenticated admin profile.
|
||||
- Update first name, last name, email, status, and optional password.
|
||||
- Show assigned roles.
|
||||
- Crop and stage an avatar image in the form UI.
|
||||
- Use module-local translations through `ACCOUNT.*` keys.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- AccountSettings owns the current-user profile UI.
|
||||
- Auth owns current-user state, token storage, and API calls used by the profile page.
|
||||
- Backend Admin owns profile persistence and validation.
|
||||
- AccountSettings should not manage other admins; that belongs to AccessControl.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Account Settings Backend
|
||||
|
||||
Account Settings does not have a separate backend base module. It uses the Admin backend module's current-user profile endpoint.
|
||||
|
||||
## Route
|
||||
|
||||
| Method | Route | Controller | Purpose |
|
||||
| ------ | ----------------------- | ------------------------------- | ----------------------------------------- |
|
||||
| `PUT` | `/admin/update-profile` | `AdminController.updateProfile` | Update the currently authenticated admin. |
|
||||
|
||||
Source:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/routes.ts
|
||||
apps/backend/app/base_modules/Admin/controller/admin_controller.ts
|
||||
```
|
||||
|
||||
The route uses `middleware.auth()`.
|
||||
|
||||
## Request Flow
|
||||
|
||||
```txt
|
||||
PUT /admin/update-profile
|
||||
-> auth middleware
|
||||
-> AdminController.updateProfile
|
||||
-> admin_update_profile_validator
|
||||
-> current auth user merge/save
|
||||
-> reload roles
|
||||
-> query role_permissions
|
||||
-> return { user, permissions }
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Validator:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/validators/admin_update_profile_validator.ts
|
||||
```
|
||||
|
||||
Keep frontend validation compatible with this validator. Backend validation is authoritative.
|
||||
|
||||
## Response Contract
|
||||
|
||||
The endpoint returns the same shape consumed by Auth:
|
||||
|
||||
```ts
|
||||
type GetUserResponse = {
|
||||
user: {
|
||||
id: number;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
roles: Array<{ id: number; name: string }>;
|
||||
};
|
||||
permissions: string[];
|
||||
};
|
||||
```
|
||||
|
||||
AuthProvider maps this response into `AuthUser` and updates current user state.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- AccountSettings owns the UI and form behavior.
|
||||
- Auth owns `updateUser` and current user state updates.
|
||||
- Backend Admin owns profile persistence and response shape.
|
||||
- AccessControl owns managing other admin accounts.
|
||||
@@ -0,0 +1,99 @@
|
||||
# Account Settings Frontend
|
||||
|
||||
The frontend module implements the current admin profile settings page.
|
||||
|
||||
Source: `apps/admin/src/core/AccountSettings/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
AccountSettings/
|
||||
config/ Routes, messages, translations.
|
||||
pages/ Profile route page.
|
||||
view/
|
||||
forms/ Profile form and form fields.
|
||||
layouts/ Account layout.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Frontend route config:
|
||||
|
||||
```txt
|
||||
config/routing.tsx
|
||||
```
|
||||
|
||||
Mounted module path:
|
||||
|
||||
```txt
|
||||
/account/*
|
||||
```
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| ------------------ | ------------- | ------------------------------- |
|
||||
| `/account/profile` | `ProfilePage` | Current admin profile settings. |
|
||||
|
||||
The index route redirects to `/account/profile`.
|
||||
|
||||
## Profile Page
|
||||
|
||||
Main file:
|
||||
|
||||
```txt
|
||||
pages/Profile/index.tsx
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- Read the current user from `useAuth()`.
|
||||
- Submit profile updates through `updateUser`.
|
||||
- Map backend validation errors into form fields.
|
||||
- Render assigned roles.
|
||||
|
||||
## Form
|
||||
|
||||
Profile form files:
|
||||
|
||||
```txt
|
||||
view/forms/ProfileForm/Component.tsx
|
||||
view/forms/ProfileForm/form.ts
|
||||
view/forms/ProfileForm/fields/PasswordField.tsx
|
||||
view/forms/ProfileForm/fields/AvatarCropperField.tsx
|
||||
```
|
||||
|
||||
Password behavior:
|
||||
|
||||
- Password fields are hidden until toggled open.
|
||||
- Password is optional.
|
||||
- If provided, password must be at least 8 characters.
|
||||
- If provided, confirmation must match.
|
||||
|
||||
Avatar behavior:
|
||||
|
||||
- The field supports local crop/preview UI.
|
||||
- The form currently stages avatar data in the form value.
|
||||
- Profile update payload strips avatar before calling `updateUser`.
|
||||
|
||||
## Auth Dependency
|
||||
|
||||
## Translations
|
||||
|
||||
Translations are module-local:
|
||||
|
||||
```txt
|
||||
config/messages.ts
|
||||
config/translation/en.json
|
||||
config/translation/uk.json
|
||||
```
|
||||
|
||||
Use only `ACCOUNT.*` keys inside this module.
|
||||
|
||||
## Common Change Guide
|
||||
|
||||
| Change | Start Here |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------- |
|
||||
| Add a profile field | `ProfileForm/form.ts`, `ProfileForm/Component.tsx`, backend profile validator |
|
||||
| Change password validation | `ProfileForm/form.ts` |
|
||||
| Change avatar UI | `ProfileForm/fields/AvatarCropperField.tsx` |
|
||||
| Change profile save behavior | `pages/Profile/index.tsx` and AuthProvider `updateUser` |
|
||||
| Change translations | `config/translation/en.json` and `config/translation/uk.json` |
|
||||
@@ -0,0 +1,39 @@
|
||||
# Auth Module
|
||||
|
||||
The Auth module owns admin authentication, current-user state, token persistence, auth routes, and route guarding.
|
||||
|
||||
## Source Map
|
||||
|
||||
| Area | Path |
|
||||
| --------------------- | ---------------------------------------------------- |
|
||||
| Frontend module | `apps/admin/src/core/Auth/` |
|
||||
| Frontend routes | `apps/admin/src/core/Auth/config/routing.tsx` |
|
||||
| Frontend provider | `apps/admin/src/core/Auth/providers/AuthProvider/` |
|
||||
| Frontend translations | `apps/admin/src/core/Auth/config/translation/` |
|
||||
| Backend module | `apps/backend/app/base_modules/Auth/` |
|
||||
| Backend admin model | `apps/backend/app/base_modules/Admin/model/admin.ts` |
|
||||
| Auth config | `apps/backend/config/auth.ts` |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| [Frontend](./frontend.md) | AuthProvider, login page, route guard, token storage, and translations. |
|
||||
| [Backend](./backend.md) | Login/logout/current-user routes, validators, token behavior, and response contracts. |
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- Sign in with admin email and password.
|
||||
- Store bearer token client-side.
|
||||
- Verify and load current authenticated admin.
|
||||
- Expose current user, permissions, login, logout, verify, and profile update methods.
|
||||
- Guard app routes until authentication is known.
|
||||
- Clear React Query cache on logout.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- Auth owns session state and current user state.
|
||||
- Auth owns token persistence.
|
||||
- Auth owns auth route UI and auth guard widget.
|
||||
- AccountSettings may call AuthProvider `updateUser` but does not own auth state.
|
||||
- Permission gates consume `AuthUser.permissions` but do not fetch auth state themselves.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Auth Backend
|
||||
|
||||
The backend Auth module exposes token-based admin authentication routes.
|
||||
|
||||
Source: `apps/backend/app/base_modules/Auth/`
|
||||
|
||||
## Routes
|
||||
|
||||
| Method | Route | Controller Method | Auth Required | Purpose |
|
||||
| ------ | -------------- | ----------------- | ------------- | ------------------------------------------- |
|
||||
| `POST` | `/auth/login` | `login` | No | Verify credentials and create bearer token. |
|
||||
| `POST` | `/auth/logout` | `logout` | Yes | Invalidate current bearer token. |
|
||||
| `GET` | `/auth/user` | `user` | Yes | Return current admin and permission slugs. |
|
||||
|
||||
Route file:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Auth/routes.ts
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
### Login
|
||||
|
||||
```txt
|
||||
POST /auth/login
|
||||
-> auth_login_validator
|
||||
-> Admin.findBy('email')
|
||||
-> hash.verify(password)
|
||||
-> Admin.accessTokens.create(admin)
|
||||
-> return { type, token, user }
|
||||
```
|
||||
|
||||
### Current User
|
||||
|
||||
```txt
|
||||
GET /auth/user
|
||||
-> auth middleware
|
||||
-> auth.use('api').user
|
||||
-> load roles
|
||||
-> query role_permissions
|
||||
-> return { user, permissions }
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Login validator:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Auth/validators/auth_login_validator.ts
|
||||
```
|
||||
|
||||
Current rules:
|
||||
|
||||
- `email`: trimmed email string.
|
||||
- `password`: trimmed string with minimum length 8.
|
||||
|
||||
## Token Guard
|
||||
|
||||
Auth config:
|
||||
|
||||
```txt
|
||||
apps/backend/config/auth.ts
|
||||
```
|
||||
|
||||
The admin model is configured as the token user provider:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/model/admin.ts
|
||||
```
|
||||
|
||||
## Response Contracts
|
||||
|
||||
Login response consumed by frontend:
|
||||
|
||||
```ts
|
||||
type LoginResponse = {
|
||||
type: "bearer";
|
||||
token: string;
|
||||
user: unknown;
|
||||
};
|
||||
```
|
||||
|
||||
Current-user response consumed by frontend:
|
||||
|
||||
```ts
|
||||
type GetUserResponse = {
|
||||
user: {
|
||||
id: number;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
roles: Array<{ id: number; name: string }>;
|
||||
};
|
||||
permissions: string[];
|
||||
};
|
||||
```
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- Auth backend owns token creation, token invalidation, and current-user response shape.
|
||||
- Admin backend owns admin persistence and profile updates.
|
||||
- Permission data in `/auth/user` is derived from role-permission rows.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Auth Frontend
|
||||
|
||||
The frontend module implements authentication screens, session provider, current-user loading, and route guards.
|
||||
|
||||
Source: `apps/admin/src/core/Auth/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
Auth/
|
||||
config/ Routes, messages, translations.
|
||||
data/
|
||||
api/ Raw auth/profile API calls and DTOs.
|
||||
mappers/ Current-user DTO mapping.
|
||||
stores/ Local token storage helpers.
|
||||
entities/ Auth and user types.
|
||||
pages/ Sign-in page.
|
||||
providers/ AuthProvider and context.
|
||||
view/
|
||||
forms/ Sign-in form.
|
||||
layouts/ Auth layouts.
|
||||
widgets/ Auth guard.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Frontend route config:
|
||||
|
||||
```txt
|
||||
config/routing.tsx
|
||||
```
|
||||
|
||||
Mounted module path:
|
||||
|
||||
```txt
|
||||
/auth/*
|
||||
```
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| -------------- | ------------ | --------------------- |
|
||||
| `/auth/signin` | `SignInPage` | Admin sign-in screen. |
|
||||
|
||||
The index route redirects to `signin`.
|
||||
|
||||
## AuthProvider
|
||||
|
||||
Provider file:
|
||||
|
||||
```txt
|
||||
providers/AuthProvider/AuthProvider.tsx
|
||||
```
|
||||
|
||||
Exposes:
|
||||
|
||||
```ts
|
||||
auth;
|
||||
user;
|
||||
loading;
|
||||
login(email, password);
|
||||
logout();
|
||||
verify();
|
||||
getUser();
|
||||
updateUser(payload);
|
||||
saveAuth(auth);
|
||||
setUser(user);
|
||||
```
|
||||
|
||||
Important behavior:
|
||||
|
||||
- `login` stores the access token, then loads the current user.
|
||||
- `verify` reloads current user when a token exists.
|
||||
- `logout` calls the backend, clears auth state, and clears React Query cache.
|
||||
- `updateUser` calls `/admin/update-profile` and updates current user state.
|
||||
|
||||
## API Boundary
|
||||
|
||||
Raw API calls:
|
||||
|
||||
```txt
|
||||
data/api/api.ts
|
||||
data/api/dto.ts
|
||||
```
|
||||
|
||||
| Method | Backend Route | Purpose |
|
||||
| ------------ | --------------------------- | ---------------------------------- |
|
||||
| `login` | `POST /auth/login` | Create bearer token. |
|
||||
| `logout` | `POST /auth/logout` | Invalidate current token. |
|
||||
| `getUser` | `GET /auth/user` | Load current user and permissions. |
|
||||
| `updateUser` | `PUT /admin/update-profile` | Update current admin profile. |
|
||||
|
||||
Current-user DTO mapping:
|
||||
|
||||
```txt
|
||||
data/mappers/userMapper.ts
|
||||
```
|
||||
|
||||
## Token Storage
|
||||
|
||||
Token persistence helpers live in:
|
||||
|
||||
```txt
|
||||
data/stores/localStorage.ts
|
||||
```
|
||||
|
||||
Keep token storage behind these helpers so storage format changes stay local.
|
||||
|
||||
## Route Guard
|
||||
|
||||
Auth guard widget:
|
||||
|
||||
```txt
|
||||
view/widgets/AuthGuardWidget.tsx
|
||||
```
|
||||
|
||||
Use this widget to protect authenticated app routes. It coordinates auth loading and redirects unauthenticated users to auth routes.
|
||||
|
||||
## Translations
|
||||
|
||||
Translations are module-local:
|
||||
|
||||
```txt
|
||||
config/messages.ts
|
||||
config/translation/en.json
|
||||
config/translation/uk.json
|
||||
```
|
||||
|
||||
Use only `AUTH.*` keys inside this module.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Devtools
|
||||
|
||||
The admin devtools module contains development-only interfaces for generating business modules, reviewing UI and form components in the sandbox, and managing the local application version.
|
||||
|
||||
Source: `apps/admin/src/devtools/`
|
||||
|
||||
The frontend module is registered through `apps/admin/src/devtools/config/module.ts`. Its routes, menu, and translations are exposed only when `IS_DEV_MODE` is enabled. All devtools frontend routes are protected routes.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Area | Purpose | Documentation |
|
||||
| ---------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------ |
|
||||
| Module generator | Generates backend and admin frontend modules from a database table and UI configuration | [Module generator](./module-generator/Readme.md) |
|
||||
| Sandbox | Interactive examples and copyable source snippets for shared UI and form controls | [Sandbox](./sandbox/Readme.md) |
|
||||
|
||||
The version manager is part of the same source module but is outside the scope of these guides.
|
||||
|
||||
## Frontend Routes
|
||||
|
||||
| Route | Page |
|
||||
| ------------------------------------ | ------------------------------ |
|
||||
| `/devtools/module-generator` | Module generator |
|
||||
| `/devtools/module-generator/docs` | Module generator documentation |
|
||||
| `/devtools/sandbox/ui` | Shared UI component sandbox |
|
||||
| `/devtools/sandbox/forms` | Composed form-control sandbox |
|
||||
| `/devtools/module-generator/version` | Version manager |
|
||||
|
||||
Devtools module is enabled only in dev mode!
|
||||
@@ -0,0 +1,53 @@
|
||||
# Module Generator
|
||||
|
||||
Creates a backend module and a matching admin frontend module from an existing PostgreSQL table, combining database metadata with frontend code builders and Handlebars templates. The output is a maintainable starting point — generated authorization, validation, relationships, API behavior, labels, and translations must still be reviewed for the product domain.
|
||||
|
||||
| Part | Source |
|
||||
| ------------------------------- | --------------------------------------------------- |
|
||||
| Generator page and form | `apps/admin/src/devtools/` |
|
||||
| Copied frontend module template | `apps/admin/src/modules/_template/` |
|
||||
| Generator HTTP endpoints | `apps/backend/start/routes.ts` |
|
||||
| Generator controller | `apps/backend/app/controllers/module_controller.ts` |
|
||||
| Database inspection | `apps/backend/app/services/database_inspector.ts` |
|
||||
| Backend Handlebars templates | `apps/backend/app/templates/backend/` |
|
||||
| Frontend Handlebars templates | `apps/backend/app/templates/frontend/` |
|
||||
|
||||
Details: [frontend.md](./frontend.md) (UI, payload builders, generated frontend), [backend.md](./backend.md) (endpoints, inspection, templates, generated backend), [troubleshooting.md](./troubleshooting.md) (module not loaded / missing from menu).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Backend running from `apps/backend/` (destinations resolve relative to `process.cwd()`); admin app in dev mode; authenticated session (all endpoints use auth middleware).
|
||||
2. The source table created and migrated — the form reads PostgreSQL `information_schema`.
|
||||
3. A module name valid as a TypeScript identifier, route segment, permission slug, and folder name.
|
||||
|
||||
## Generation Flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["Select table and assets"] --> B["Configure form and table"]
|
||||
B --> C["Frontend builders create source strings"]
|
||||
C --> D["POST /makeModule/generate"]
|
||||
D --> E["Inspect database metadata"]
|
||||
E --> F["Copy frontend _template"]
|
||||
E --> G["Render backend templates"]
|
||||
F --> H["Write configured frontend files"]
|
||||
G --> I["Write backend module and tests"]
|
||||
```
|
||||
|
||||
Output goes to `apps/backend/modules/<ModuleName>/`, `apps/backend/tests/modules/<ModuleName>/`, and `apps/admin/src/modules/<name>/`. Frontend modules are discovered through their `config/module.ts` manifest; backend routes load from `apps/backend/modules/` at startup.
|
||||
|
||||
## Operational Behavior
|
||||
|
||||
- Generation writes directly into both workspaces — no preview, transaction, or rollback; existing files may be overwritten. Generate into a clean name and inspect the Git diff.
|
||||
- Select `model` whenever you select `api` — API generation imports the model for its Swagger schema.
|
||||
- The frontend template is always copied; form and datatable files are populated only when selected.
|
||||
- Generated code is not formatted or committed automatically.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
1. Inspect the Git diff; confirm route paths, API prefixes, permission slugs, and naming.
|
||||
2. Review nullable fields, dates, passwords, foreign keys, validation, relationship names; replace generic labels and translations.
|
||||
3. Verify list/create/update/delete, authorization, and validation-error behavior.
|
||||
4. Run `npm run build:backend` and `npm run build:admin`; run the generated backend tests and add domain coverage.
|
||||
|
||||
Generated frontend follows the [React frontend architecture](../../../architecture/react-frontend/Readme.md); review backend modules against the [backend architecture](../../../architecture/backend/Readme.md).
|
||||
@@ -0,0 +1,61 @@
|
||||
# Module Generator Backend
|
||||
|
||||
Authenticated schema-inspection and generation endpoints; backend files rendered from Handlebars templates, frontend files written from a copied template plus source strings supplied by the admin app.
|
||||
|
||||
## Endpoints
|
||||
|
||||
Registered in `apps/backend/start/routes.ts`, all behind `middleware.auth()`:
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| ------ | ---------------------------- | -------------------------------------------------- |
|
||||
| `GET` | `/makeModule/tables` | List eligible public PostgreSQL tables |
|
||||
| `GET` | `/makeModule/columns/:table` | Return columns used to configure form/table fields |
|
||||
| `POST` | `/makeModule/generate` | Generate selected frontend and backend assets |
|
||||
|
||||
## Database Inspection
|
||||
|
||||
`database_inspector.ts` reads `information_schema`: only the `public` schema, excluding framework/auth/access-control/migration/system tables. Columns ending in `_id` become belongs-to candidates; tables containing a `<table>_id` column become has-many candidates; searchable string/numeric columns are derived for resource controllers. **Relationship inference is heuristic** — review generated model imports, class and property names.
|
||||
|
||||
## Type Mapping
|
||||
|
||||
| Database category | Lucid TypeScript type | Vine type | Frontend type |
|
||||
| --------------------- | --------------------- | --------- | --------------- |
|
||||
| integer/numeric types | `number` | `number` | `number` |
|
||||
| boolean | `boolean` | `boolean` | `boolean` |
|
||||
| date/timestamp | `DateTime` | `date` | `IsoDateString` |
|
||||
| other values | `string` | `string` | `string` |
|
||||
|
||||
Nullable columns get `| null` and optional validation; `created_at`, `updated_at`, `deleted_at` receive special handling.
|
||||
|
||||
## Backend Output
|
||||
|
||||
```txt
|
||||
apps/backend/modules/<ModuleName>/
|
||||
├── api/ # controller, create/update validators, routes.ts
|
||||
├── config/permissions.ts
|
||||
├── controller/<entity>_controller.ts
|
||||
├── model/<entity>.ts
|
||||
├── policy/<entity>_policy.ts
|
||||
├── validators/ # <entity>_create/_update_validator.ts
|
||||
└── routes.ts
|
||||
|
||||
apps/backend/tests/modules/<ModuleName>/ # <entity>.spec.ts, <entity>.api.spec.ts
|
||||
```
|
||||
|
||||
`<entity>` is the table name minus its final character — assumes a plural table name; review irregular plurals.
|
||||
|
||||
## Asset Behavior
|
||||
|
||||
- **Model** — `model.hbs` with columns, inferred relationships, searchable fields, optional password hashing.
|
||||
- **Controller and routes** — authenticated resource controller with list/search/sort/pagination and CRUD; `routes.hbs` registers an `apiOnly` resource route with authentication.
|
||||
- **Validators** — separate create/update Vine validators; primary keys and timestamp/deletion fields excluded; email, password, nullable, and trimming behavior inferred from names and metadata.
|
||||
- **Policy and permissions** — Bouncer policy with view/create/update/delete, `config/permissions.ts`, and a lazy policy import added to `app/policies/main.ts` when missing. Slugs derive from the module name (`<name>s.view`, `.create`, `.update`, `.delete`) — review already-plural names, this is not a general pluralization system.
|
||||
- **API subtree** — a second CRUD controller and routes under `/api/<entity>` with validators and a test; imports the model for `modelToSchema` Swagger metadata. **The generated API template adds no authentication middleware — add auth and authorization before exposing the route.**
|
||||
|
||||
## Frontend Writes
|
||||
|
||||
The backend copies `apps/admin/src/modules/_template` to `apps/admin/src/modules/<name>`, then writes the configured form (component + definition), datatable and list page, manage trigger, menu, module manifest, generated `config/meta.ts`, and database-derived `entities/Entity.ts`. See [frontend.md](./frontend.md).
|
||||
|
||||
## Extending
|
||||
|
||||
Handlebars helpers available: `eq`, `not`, `or`, `and`. To add a generated artifact: add the template under `apps/backend/app/templates/`, an explicit destination in the controller `paths` object (parent dir via existing path initialization), render only when its asset is selected, and pass normalized metadata — never query the database inside templates. Add generation tests for nullable values, dates, numerics, passwords, and irregular names; verify both builds. No hidden writes — every destination, including shared registries like `app/policies/main.ts`, stays visible in the controller flow and documented here.
|
||||
@@ -0,0 +1,69 @@
|
||||
# Module Generator Frontend
|
||||
|
||||
The generator UI collects module metadata, selected assets, form layout, and datatable configuration, converts it into TypeScript/TSX source strings, and sends them to the backend generator.
|
||||
|
||||
| Responsibility | Source (under `apps/admin/src/devtools/`) |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| Page submission, notifications, and mutation callbacks | `pages/ModuleGeneratorPage/index.tsx` |
|
||||
| Payload assembly | `pages/ModuleGeneratorPage/lib/buildGenerateModulePayload.ts` |
|
||||
| Form state, initial values, and sections | `view/forms/ModuleGeneratorForm/` |
|
||||
| Payload DTOs and HTTP client | `data/api/` |
|
||||
| React Query hooks | `data/queries/` |
|
||||
| Source-code builders | `data/builders/` |
|
||||
|
||||
The page submits through `useGenerateModule`; success resets the form and shows an alert, errors go through the shared React Query error helpers.
|
||||
|
||||
## Metadata and Assets
|
||||
|
||||
Required metadata: `name` (frontend folder, backend module identity, route segment, permission-slug base), `table` (existing PostgreSQL table), `generate` (selected assets). Backend assets are all selected by default; form and datatable are opt-in.
|
||||
|
||||
| Asset | Workspace | Output |
|
||||
| ------------ | --------- | --------------------------------------------------- |
|
||||
| `model` | Backend | Lucid model |
|
||||
| `controller` | Backend | Authenticated resource controller and test |
|
||||
| `routes` | Backend | Resource route registration |
|
||||
| `validator` | Backend | Create and update Vine validators |
|
||||
| `policy` | Backend | Bouncer policy and permission configuration |
|
||||
| `api` | Backend | API controller, validators, routes, and test |
|
||||
| `form` | Frontend | Form definition, component, and modal/sheet trigger |
|
||||
| `datatable` | Frontend | List page table and menu entry |
|
||||
|
||||
Before generation the UI reads `GET /makeModule/tables` (table selector) and `GET /makeModule/columns/:table` (field choices) — cached via React Query, columns disabled until a table is selected.
|
||||
|
||||
## Payload Builders
|
||||
|
||||
`buildGenerateModulePayload` delegates to focused builders:
|
||||
|
||||
| Builder | Payload field | Generated destination |
|
||||
| ----------------------------------- | ---------------- | ----------------------------------- |
|
||||
| `FormSourceBuilder.buildComponent` | `form` | `view/forms/MainForm/Component.tsx` |
|
||||
| `FormSourceBuilder.buildDefinition` | `formDefinition` | `view/forms/MainForm/form.ts` |
|
||||
| `ManageTriggerSourceBuilder` | `formDialog` | `view/widgets/ManageTrigger.tsx` |
|
||||
| `DataTableSourceBuilder` | `dataTable` | `view/widgets/DataTable.tsx` |
|
||||
| `ListPageSourceBuilder` | `list` | `pages/ListPage/index.tsx` |
|
||||
| `MenuSourceBuilder` | `menu` | `config/menu.ts` |
|
||||
| `ModuleConfigSourceBuilder` | `module` | `config/module.ts` |
|
||||
|
||||
Form disabled → `form`, `formDefinition`, `formDialog` are `null`. Datatable disabled → `dataTable` and `menu` are `null`, and the list-page builder emits a page without a table.
|
||||
|
||||
## Generated Frontend Architecture
|
||||
|
||||
```txt
|
||||
apps/admin/src/modules/<name>/
|
||||
├── api/ # create.ts, delete.ts, detailed.ts, keys.ts, list.ts, update.ts
|
||||
├── config/ # translation/, menu.ts, messages.ts, meta.ts, module.ts, routing.tsx
|
||||
├── entities/ # Entity.ts (from database columns)
|
||||
├── pages/ListPage/
|
||||
└── view/
|
||||
├── forms/MainForm/ # Component.tsx, constants.ts, form.ts, index.ts, submit.ts
|
||||
├── layouts/Layout.tsx
|
||||
└── widgets/ # DataTable.tsx, DeleteTrigger.tsx, ManageTrigger.tsx
|
||||
```
|
||||
|
||||
`api/` deliberately has no barrel file — each CRUD operation is its own file, imported directly (`import { useCreate } from "../../../api/create"`); `keys.ts` owns query key builders, `list.ts`/`detailed.ts` their invalidation helpers.
|
||||
|
||||
Layer ownership: `entities/Entity.ts` is shared by API hooks, forms, and widgets; `api/` owns Axios calls, hooks, keys, and notifications; `pages/` owns route composition and permission gates; `view/forms/` owns schema, values, UI, and submission; `view/widgets/` owns stateful interactions; `config/` integrates routes, menu, translations, and permissions with the module registry.
|
||||
|
||||
## Extending
|
||||
|
||||
To add a generated frontend artifact: add its configuration type under `entities/`, UI controls to `ModuleGeneratorForm`, a focused builder under `data/builders/` (for dynamic source), the payload field to `GenerateModulePayload` + `buildGenerateModulePayload`, accept and write it in `module_controller.ts`, and add static structure to `_template/` when every module needs it. Verify generation with and without the asset selected.
|
||||
@@ -0,0 +1,31 @@
|
||||
# Module Generator Troubleshooting
|
||||
|
||||
## Generated Files Exist but the Module Is Not Loaded
|
||||
|
||||
Docker file watching does not reliably pick up entirely new directories: the backend discovers `apps/backend/modules/` routes at startup, and the admin app discovers `apps/admin/src/modules/**/config/module.ts` through Vite's module registry. Restart both services and reload the admin app:
|
||||
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
Still not loaded? Check logs for TypeScript errors, invalid generated imports, route-loading or manifest errors:
|
||||
|
||||
```bash
|
||||
docker compose logs --tail=200 backend admin
|
||||
```
|
||||
|
||||
(Outside Compose, restart the backend and admin dev processes manually.)
|
||||
|
||||
## Module Loads but Is Missing from the Menu
|
||||
|
||||
Usually a permission mismatch or invalid menu/module configuration. Check in order:
|
||||
|
||||
1. **Permission slugs match exactly** — backend `apps/backend/modules/<ModuleName>/config/permissions.ts` (e.g. `{ slug: 'products.view' }`) vs frontend `apps/admin/src/modules/<name>/config/meta.ts` (`MODULE_PERMISSION_VIEW = 'products.view'`), for view/create/update/delete. Pluralization is heuristic — already-plural or irregular names need manual correction.
|
||||
2. **The current role owns the view permission** — the menu filters on `MODULE_PERMISSION_VIEW`: the permission exists in access control, the role has it with the exact slug, and the session was refreshed after role changes.
|
||||
3. **`config/menu.ts`** — valid exported menu factory, `path` uses `MODULE_ROOT_PATH`, `permissions` contains `MODULE_PERMISSION_VIEW`, the route exists. The menu is generated with the Datatable asset — without it, add a menu manually or keep the module hidden.
|
||||
4. **`config/module.ts`** — exports a valid `AppModuleConfig`, unique module ID, not `enabled: false`, exposes `menu`, `routes`, `messages`.
|
||||
5. **`config/routing.tsx`** — `MODULE_ROOT_PATH`, the menu path, and the generated route all point to the same URL (a broken route and an invisible menu item are separate problems).
|
||||
|
||||
## Final Checks
|
||||
|
||||
Run `npm run build:backend` and `npm run build:admin` to expose generated TypeScript or import errors, then verify: both module directories exist with the selected assets, services were restarted, slugs match, the role has the view permission, and the menu path matches the route. Module discovery errors are reported in backend and admin startup logs.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Devtools Sandbox
|
||||
|
||||
The sandbox is an interactive development surface for reviewing shared admin UI components and composed form controls. Each example renders the real component and exposes a copyable usage snippet.
|
||||
|
||||
## Routes and Source
|
||||
|
||||
| Route | Source | Coverage |
|
||||
| ------------------------- | ------------------------------------------------- | --------------------------------------------- |
|
||||
| `/devtools/sandbox/ui` | `apps/admin/src/devtools/pages/SandboxUIPage/` | Shared UI primitives and composed UI behavior |
|
||||
| `/devtools/sandbox/forms` | `apps/admin/src/devtools/pages/SandboxFormsPage/` | Shared composed form controls |
|
||||
|
||||
## Maintenance Rules
|
||||
|
||||
- Use real shared components; do not duplicate their implementation in devtools.
|
||||
- Keep examples small enough to show one API or behavior clearly.
|
||||
- Keep rendered examples and source snippets synchronized.
|
||||
- Prefer adding edge-state examples when a component has loading, empty, error, disabled, or destructive behavior.
|
||||
- Do not add backend dependencies to the sandbox unless the example explicitly documents and isolates them.
|
||||
- Remove sandbox examples when the underlying shared API is removed.
|
||||
@@ -0,0 +1,47 @@
|
||||
# Fallback Screens Module
|
||||
|
||||
The Fallback Screens module owns route-level and reusable fallback pages in the admin app. It currently provides HTTP-style error screens for forbidden, not found, and server error states, and it is the intended home for future fallback pages such as maintenance, unavailable, empty-state, or generic recovery screens.
|
||||
|
||||
## Source Map
|
||||
|
||||
| Area | Path |
|
||||
| --------------------- | ------------------------------------------------------------ |
|
||||
| Frontend module | `apps/admin/src/core/FallbackScreens/` |
|
||||
| Frontend routes | `apps/admin/src/core/FallbackScreens/config/routing.tsx` |
|
||||
| Frontend translations | `apps/admin/src/core/FallbackScreens/config/translation/` |
|
||||
| Frontend layout | `apps/admin/src/core/FallbackScreens/view/layouts/` |
|
||||
| Backend module | Not applicable. Fallback Screens is currently frontend-only. |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------- | ----------------------------------------------------------------- |
|
||||
| [Frontend](./frontend.md) | Routes, page structure, translations, ownership, and conventions. |
|
||||
| [Backend](./backend.md) | Backend ownership notes and when a backend change may be needed. |
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- Render full-page fallback screens under `/error/*`.
|
||||
- Provide reusable fallback page components for other core modules.
|
||||
- Keep fallback screen translations module-local.
|
||||
- Provide a consistent layout for fallback pages.
|
||||
- Preserve stable public fallback URLs such as `/error/403`, `/error/404`, and `/error/500`.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- FallbackScreens owns fallback page UI, fallback routing, and fallback-specific translations.
|
||||
- Other modules may import reusable fallback pages, for example permission gates using `Error403Page`.
|
||||
- Shared API/query error parsing does not belong here; keep it in shared error/query utilities.
|
||||
- Domain-specific empty states should stay in the owning domain module unless they are generic enough to reuse across modules.
|
||||
- Backend modules own the actual API status codes and response payloads. FallbackScreens only decides how the admin frontend presents fallback states.
|
||||
|
||||
## Common Change Guide
|
||||
|
||||
| Change | Start Here |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------ |
|
||||
| Add a new fallback route | `apps/admin/src/core/FallbackScreens/config/routing.tsx` |
|
||||
| Add a new fallback page | `apps/admin/src/core/FallbackScreens/pages/` |
|
||||
| Change fallback page layout | `apps/admin/src/core/FallbackScreens/view/layouts/FallbackScreensLayout.tsx` |
|
||||
| Change translations | `apps/admin/src/core/FallbackScreens/config/translation/en.json` and `uk.json` |
|
||||
| Reuse a fallback in a module | Export it from `apps/admin/src/core/FallbackScreens/index.ts` |
|
||||
| Change global fallback routing | `apps/admin/src/app/routing/app_routing_setup.tsx` |
|
||||
@@ -0,0 +1,29 @@
|
||||
# Fallback Screens Backend
|
||||
|
||||
Fallback Screens is currently a frontend-only core module.
|
||||
|
||||
There is no backend source folder for this module.
|
||||
|
||||
## Backend Relationship
|
||||
|
||||
Backend modules may return HTTP status codes or API error payloads that eventually lead the admin frontend to show a fallback screen, but the fallback UI is owned by:
|
||||
|
||||
```txt
|
||||
apps/admin/src/core/FallbackScreens/
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
| Status | Frontend fallback | Notes |
|
||||
| ------ | ---------------------- | --------------------------------------------- |
|
||||
| `403` | `/error/403` or inline | Used for permission denied states. |
|
||||
| `404` | `/error/404` | Used for unknown frontend routes. |
|
||||
| `500` | `/error/500` | Used for generic server failure presentation. |
|
||||
|
||||
## When Backend Changes Are Needed
|
||||
|
||||
Backend changes are only needed when:
|
||||
|
||||
- A backend endpoint should return a different status code.
|
||||
- A redirect or auth flow should navigate the frontend to a fallback route.
|
||||
- A feature needs a new machine-readable error contract that the frontend can map to a fallback screen.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Fallback Screens Frontend
|
||||
|
||||
The frontend module provides fallback UI for app-level error and recovery states.
|
||||
|
||||
Source: `apps/admin/src/core/FallbackScreens/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
FallbackScreens/
|
||||
config/ Routes, messages, and translations.
|
||||
pages/ Route-level fallback pages.
|
||||
view/
|
||||
layouts/ Shared fallback page layout.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Frontend route config:
|
||||
|
||||
```txt
|
||||
config/routing.tsx
|
||||
```
|
||||
|
||||
Mounted route path:
|
||||
|
||||
```txt
|
||||
/error/*
|
||||
```
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| ------------ | -------------- | ------------------------------------ |
|
||||
| `/error` | `Error404Page` | Default fallback route. |
|
||||
| `/error/403` | `Error403Page` | Forbidden or permission denied page. |
|
||||
| `/error/404` | `Error404Page` | Not found page. |
|
||||
| `/error/500` | `Error500Page` | Internal server error page. |
|
||||
|
||||
Unknown `/error/*` paths redirect to `/error/404`.
|
||||
|
||||
## App Integration
|
||||
|
||||
The app-level router mounts `fallbackScreensRoute` from:
|
||||
|
||||
```txt
|
||||
apps/admin/src/app/routing/app_routing_setup.tsx
|
||||
```
|
||||
|
||||
The app-level i18n config merges `fallbackScreensMessages` from:
|
||||
|
||||
```txt
|
||||
apps/admin/src/app/i18n/config.ts
|
||||
```
|
||||
|
||||
## Translations
|
||||
|
||||
Translations are module-local:
|
||||
|
||||
```txt
|
||||
config/messages.ts
|
||||
config/translation/en.json
|
||||
config/translation/uk.json
|
||||
```
|
||||
|
||||
Use only `FALLBACK_SCREENS.*` keys inside this module.
|
||||
|
||||
Do not put fallback page strings in app-level files such as:
|
||||
|
||||
```txt
|
||||
apps/admin/src/app/i18n/messages/en.json
|
||||
apps/admin/src/app/i18n/messages/uk.json
|
||||
```
|
||||
|
||||
## Page Conventions
|
||||
|
||||
- Keep route-level fallback pages under `pages/*Page/index.tsx`.
|
||||
- Keep shared wrapper layout under `view/layouts/`.
|
||||
- Keep public URLs stable unless there is a migration plan.
|
||||
- Prefer module-local translation keys for all visible text, including generic-looking actions like return-home links.
|
||||
- Add a route only when the fallback is intended to be navigable. Inline-only reusable fallbacks can be exported without a route.
|
||||
@@ -0,0 +1,102 @@
|
||||
# File Manager Module
|
||||
|
||||
Authenticated file browsing and file operations for the admin app: a backend API for storage operations and a frontend UI for browsing, previewing, uploading, renaming, deleting, duplicating, and editing supported files.
|
||||
|
||||
## Source Map
|
||||
|
||||
| Area | Path |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------ |
|
||||
| Backend module | `apps/backend/app/base_modules/FileManager/` |
|
||||
| Backend storage config | `apps/backend/config/drive.ts` |
|
||||
| Backend S3 adapter | `apps/backend/app/base_modules/FileManager/services/storage_service/s3_storage_service.ts` |
|
||||
| Frontend module | `apps/admin/src/core/FileManager/` |
|
||||
| Frontend file assets | `apps/admin/public/media/files/` |
|
||||
| Frontend routes | `apps/admin/src/core/FileManager/config/routing.tsx` |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------- |
|
||||
| [Backend](./backend.md) | API routes, services, storage adapter contract, permissions, and extension points. |
|
||||
| [Frontend](./frontend.md) | UI structure, React Query and MobX, cache updates, pages, and widgets. |
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- Browse, filter, sort, paginate, and search folders and files.
|
||||
- Preview details and text content; stream raw files.
|
||||
- Create folders; upload with progress and cancellation; rename, delete, duplicate entries.
|
||||
- Image editor and image map zones for supported image files.
|
||||
- Reusable picker components for choosing files and inserting them into the RichText editor.
|
||||
- Local filesystem or S3-compatible storage through Adonis Drive.
|
||||
|
||||
## High-Level Flow
|
||||
|
||||
```txt
|
||||
Admin route
|
||||
-> FileManager frontend page
|
||||
-> React Query API hook
|
||||
-> /file-manager backend route
|
||||
-> FileManagerController
|
||||
-> FileManagerService
|
||||
-> StorageService adapter
|
||||
-> Adonis Drive fs or s3 disk
|
||||
```
|
||||
|
||||
## Data Contract
|
||||
|
||||
Backend responses use `Entry` objects:
|
||||
|
||||
```ts
|
||||
type Entry = FileEntry | FolderEntry;
|
||||
|
||||
type FileEntry = {
|
||||
type: "file";
|
||||
name: string;
|
||||
path: string;
|
||||
url: string;
|
||||
size?: number;
|
||||
updatedAt?: string;
|
||||
};
|
||||
|
||||
type FolderEntry = {
|
||||
type: "folder";
|
||||
name: string;
|
||||
path: string;
|
||||
updatedAt?: string;
|
||||
};
|
||||
```
|
||||
|
||||
The frontend maps DTOs into frontend entities and adds UI-oriented metadata such as `parent` and
|
||||
`extension`.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- Backend owns storage, validation, authorization, path safety, and API response shape.
|
||||
- Frontend owns navigation, selection, view settings, operation progress, and user interactions.
|
||||
- React Query owns server state in the frontend.
|
||||
- MobX stores own local UI and operation state in the frontend.
|
||||
- Storage adapters must implement `storage_service_interface.ts`; callers should depend on that
|
||||
interface, not a concrete disk implementation.
|
||||
- The active storage adapter is selected by `DRIVE_DISK`.
|
||||
|
||||
## Common Change Guide
|
||||
|
||||
| Change | Start Here |
|
||||
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Add or change an API route | `apps/backend/app/base_modules/FileManager/routes.ts` and `controller/file_manager_controller.ts` |
|
||||
| Change path validation or naming rules | `services/path_service.ts` |
|
||||
| Change list filtering, sorting, or pagination | `services/entry_list_service.ts` and frontend query payloads |
|
||||
| Add or change a storage backend | `services/storage_service/storage_service_interface.ts`, `create_storage_service.ts`, and [Backend storage docs](./backend.md#storage) |
|
||||
| Add a frontend operation | `apps/admin/src/core/FileManager/data/queries/` and relevant store/widget |
|
||||
| Add file type icons | `apps/admin/public/media/files/` and `shared/file/fileTypeImportMap.ts` |
|
||||
| Add preview support | Backend `file_preview_service.ts` and frontend `view/ui/FilePreview/` |
|
||||
|
||||
## Planned Work
|
||||
|
||||
- Add more non-filesystem storage adapters such as:
|
||||
- Google Cloud Storage
|
||||
- Supabase Storage
|
||||
- Digital Ocean Spaces
|
||||
- Cloudflare R2
|
||||
- Add testing notes once module-level backend and frontend tests are established.
|
||||
- Add libs for rendering large lists.
|
||||
@@ -0,0 +1,110 @@
|
||||
# File Manager Backend
|
||||
|
||||
Authenticated file operations under `/file-manager`: validation, authorization, path normalization, orchestration, with raw storage delegated to a storage adapter. Source: `apps/backend/app/base_modules/FileManager/`.
|
||||
|
||||
## Request Flow
|
||||
|
||||
```txt
|
||||
routes.ts
|
||||
-> controller/file_manager_controller.ts
|
||||
-> validator/*
|
||||
-> policy/file_manager_policy.ts
|
||||
-> services/file_manager_service.ts
|
||||
-> services/storage_service/ (interface -> factory -> fs | s3 adapter)
|
||||
-> Adonis Drive
|
||||
```
|
||||
|
||||
The controller stays thin (auth checks, validation, path extraction, status codes, response shape). `FileManagerService` owns orchestration; storage adapters own raw persistence.
|
||||
|
||||
## Routes
|
||||
|
||||
All prefixed with `/file-manager`, behind `middleware.auth()`; controller methods check policy abilities (`policy/file_manager_policy.ts`) before doing work. When adding an operation, add or reuse an explicit ability first.
|
||||
|
||||
| Method | Route | Controller Method | Ability | Purpose |
|
||||
| -------- | ------------------ | ----------------- | ---------- | ------------------------------------------------------- |
|
||||
| `GET` | `/` and `/*` | `index` | `viewList` | List entries for root or a nested directory. |
|
||||
| `GET` | `/files/details/*` | `show` | `view` | Return file metadata and optional text preview content. |
|
||||
| `GET` | `/files/raw/*` | `raw` | `view` | Stream raw file content. |
|
||||
| `GET` | `/search?q=value` | `search` | `viewList` | Search entries recursively. |
|
||||
| `POST` | `/folders` | `createFolder` | `create` | Create a folder. |
|
||||
| `POST` | `/files` | `upload` | `create` | Upload a file. |
|
||||
| `POST` | `/files/duplicate` | `duplicate` | `create` | Duplicate one or more **file** entries. |
|
||||
| `PUT` | `/entries/rename` | `rename` | `update` | Rename or move an entry within its parent path. |
|
||||
| `DELETE` | `/*` | `destroy` | `delete` | Delete an entry. |
|
||||
|
||||
## Validation
|
||||
|
||||
Validators in `validator/`: `list_validator.ts` (filter, sort, order, max results, pagination token), `create_directory_validator.ts`, `rename_entry_validator.ts`, `upload_file_validator.ts`, `duplicate_entries_validator.ts`, plus shared helpers in `schemas.ts`.
|
||||
|
||||
Path extraction from wildcard routes goes through `helpers/get_validated_path_from_request.ts` — never parse wildcard paths in controllers or storage adapters.
|
||||
|
||||
## Services
|
||||
|
||||
| Service | Owns |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `file_manager_service.ts` | Orchestration: listing with filter/sort/pagination, file detail + preview composition, moves, unique upload and copy names, search, storage calls. |
|
||||
| `path_service.ts` | Client↔storage path conversion, safe joins, dirname/basename, extension metadata, filename sanitizing, unique names. Use it instead of string manipulation. |
|
||||
| `entry_list_service.ts` | List behavior on raw storage entries: category filter, sort, folders-first, pagination + token encoding. Frontend list controls must stay compatible with its accepted values. |
|
||||
| `file_preview_service.ts` | Text preview content for previewable extensions (defined in the same file). For rich previews, keep the response small and stream via `/files/raw/*`. |
|
||||
| `entry_search_service.ts` | Path search including matching parent folders for context. Receives an abort signal from the controller — long searches should respect it. |
|
||||
|
||||
## Storage
|
||||
|
||||
Abstractions in `services/storage_service/`:
|
||||
|
||||
- `storage_service_interface.ts` — the adapter contract: list (flat and recursive), existence, metadata, read/stream/write, create directory, move, delete, copy. Services call the interface, never a concrete adapter.
|
||||
- `fs_storage_service.ts` — filesystem adapter over the Adonis Drive `fs` disk: maps filesystem objects to entries, handles streams and metadata, preserves directory marker behavior.
|
||||
- `s3_storage_service.ts` — S3-compatible adapter over the `s3` disk: maps objects and common prefixes to entries, uploads multipart files by streaming the temp file via `putStream`, creates folders with directory marker objects, moves folders by moving every object under the prefix, deletes files exactly and folders by prefix, copies file entries. S3 has no native directories, so it shares the `.keep` marker convention. **Folder operations must use slash-terminated prefixes** — deleting `test/` must not delete `test123.txt`.
|
||||
- `fs_directory_marker_service.ts` — encapsulates the `.keep` marker convention; never duplicate marker logic outside it.
|
||||
- `create_storage_service.ts` — factory; the active adapter (`fs` or `s3`) is selected by `DRIVE_DISK`.
|
||||
|
||||
## Drive Configuration
|
||||
|
||||
`apps/backend/config/drive.ts` configures two disks: `fs` (local development, filesystem storage) and `s3` (S3-compatible object storage, including local MinIO).
|
||||
|
||||
S3 environment variables:
|
||||
|
||||
| Variable | Local Docker Value | Purpose |
|
||||
| ----------------------- | --------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `AWS_ACCESS_KEY_ID` | `minioadmin` | S3 access key. |
|
||||
| `AWS_SECRET_ACCESS_KEY` | `minioadmin` | S3 secret key. |
|
||||
| `AWS_REGION` | `us-east-1` | S3 region. |
|
||||
| `S3_BUCKET` | `crux-drive` | Bucket used by the file manager. |
|
||||
| `S3_ENDPOINT` | `http://minio:9000` in Docker, `http://localhost:9000` outside Docker | S3 API endpoint. |
|
||||
| `S3_CDN_URL` | `http://localhost:9000/crux-drive/` | Browser-facing public URL base for file URLs. |
|
||||
|
||||
For MinIO the disk uses `forcePathStyle: true` and `supportsACL: false`; bucket public-read is handled by the Docker bucket setup container.
|
||||
|
||||
## Local S3 Simulation
|
||||
|
||||
`docker-compose.yml` runs `minio` (the S3 server) and `minio_create_bucket` (one-shot setup that creates the bucket and enables anonymous download — it exits after setup, which is normal). Console at `http://localhost:9001`, credentials `minioadmin` / `minioadmin`. After dependency or Dockerfile changes: `docker compose build --no-cache && docker compose up`.
|
||||
|
||||
## Response Shape
|
||||
|
||||
```ts
|
||||
interface EntryBase {
|
||||
name: string;
|
||||
path: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
interface FileEntry extends EntryBase {
|
||||
type: "file";
|
||||
size?: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface FolderEntry extends EntryBase {
|
||||
type: "folder";
|
||||
}
|
||||
```
|
||||
|
||||
List responses: `{ data: Entry[]; meta: { nextPaginationToken: string | null } }`.
|
||||
|
||||
## Backend Change Checklist
|
||||
|
||||
1. Confirm the policy ability for the operation.
|
||||
2. Add or update a validator for all request input.
|
||||
3. Keep client paths and storage paths separated through `path_service.ts`.
|
||||
4. Update the frontend DTO or mapper if the response shape changes.
|
||||
5. Update this documentation when routes, permissions, storage contracts, or response shapes change.
|
||||
@@ -0,0 +1,123 @@
|
||||
# File Manager Frontend
|
||||
|
||||
Admin file manager UI: browsing, previews, uploads, delete/duplicate/rename, image editing and image map tools, settings, and reusable file pickers. Source: `apps/admin/src/core/FileManager/`.
|
||||
|
||||
**Main principle: React Query owns server state** (entries, file details, raw blobs, search, mutations); **MobX owns local UI and operation state** (active/selected/expanded paths, filters and sort, dialog state, upload/delete progress, loaded-entry lookup).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
FileManager/
|
||||
config/ Routes, menu entries, messages, translations.
|
||||
data/
|
||||
api/ Raw HTTP methods and DTOs.
|
||||
hooks/ Feature hooks that combine queries with stores.
|
||||
mappers/ DTO-to-entity mapping.
|
||||
queries/ React Query hooks, keys, mutations, and cache helpers.
|
||||
stores/ MobX local state stores.
|
||||
entities/ Domain entities and settings types.
|
||||
providers/ Feature providers and context hooks.
|
||||
pages/ Route-level screens.
|
||||
shared/ FileManager utilities that do not depend on view code.
|
||||
view/ UI components, widgets, forms, and layouts.
|
||||
```
|
||||
|
||||
## Routes
|
||||
|
||||
Module mounted at `/file-manager/*`, configured in `config/routing.tsx`. Menu groups: `Drive` (browsing, viewer), `Tools` (image editor, image map), `Settings`.
|
||||
|
||||
| Route | Page | Purpose |
|
||||
| -------------------------------------- | ------------------- | ------------------------------------------- |
|
||||
| `/file-manager/drive/explorer/*` | `DriveExplorerPage` | Browse folders and files. |
|
||||
| `/file-manager/drive/viewer/-/*` | `DriveViewerPage` | View file details and preview. |
|
||||
| `/file-manager/drive/settings` | `DriveSettingsPage` | Configure file-manager UI settings. |
|
||||
| `/file-manager/tools/image-editor/-/*` | `ImageEditorPage` | Edit image files. |
|
||||
| `/file-manager/tools/image-map/-/*` | `ImageMapPage` | Create and edit image map areas for images. |
|
||||
|
||||
Build file-manager URLs with the helpers in `shared/routing/driveLinks.ts`, `shared/routing/openEntryNewTab.ts`, `shared/path/leadingSlash.ts` — never manually.
|
||||
|
||||
## API Boundary
|
||||
|
||||
Raw backend calls are centralized in `data/api/api.ts` + `data/api/dto.ts`, calling the routes documented in [backend.md](./backend.md):
|
||||
|
||||
| API Method | Backend Route |
|
||||
| ----------------- | ------------------------------------ |
|
||||
| `getEntries` | `GET /file-manager/*` |
|
||||
| `getFile` | `GET /file-manager/files/details/*` |
|
||||
| `getRawFile` | `GET /file-manager/files/raw/*` |
|
||||
| `createDirectory` | `POST /file-manager/folders` |
|
||||
| `renameEntry` | `PUT /file-manager/entries/rename` |
|
||||
| `removeEntry` | `DELETE /file-manager/*` |
|
||||
| `uploadFile` | `POST /file-manager/files` |
|
||||
| `duplicateFiles` | `POST /file-manager/files/duplicate` |
|
||||
| `searchEntries` | `GET /file-manager/search` |
|
||||
|
||||
DTOs are mapped before reaching UI code (`entities/entry.ts`, `data/mappers/entryMapper.ts`).
|
||||
|
||||
## Data Flow
|
||||
|
||||
- **Reading entries** — `useEntries()` builds the query payload from `ExplorerStore`, fetches via React Query (`useGetEntries`), and syncs loaded entries into `EntriesStore`. `EntriesStore` is not the fetch owner — it is an index of loaded entries for the sidebar tree, selection, active-entry lookup, and operation state.
|
||||
- **Deep links** — for a nested folder opened directly, `useEntries()` preloads ancestor folders so the sidebar tree and entry index work for paths like `/folder/subfolder/current`.
|
||||
- **Mutations** — `useCreateDirectory`, `useRenameEntry`, `useDeleteEntry`, `useDuplicateFiles`, `useUploadFile`. After success, the UI updates the local cache and stores directly instead of invalidating entries queries — no refetch storm when many items are created, deleted, or uploaded in sequence.
|
||||
|
||||
## Query Cache Updates
|
||||
|
||||
`data/queries/cache.ts` updates existing React Query cache data in memory (no network request):
|
||||
|
||||
```ts
|
||||
addEntriesToCache(parentPath, entries); // filter-aware insertion into the parent folder's cached lists
|
||||
removeEntriesFromCache(paths); // removal from all cached entry pages
|
||||
renameEntryInCache(prevPath, renamedEntry);
|
||||
```
|
||||
|
||||
Tradeoff: since the module avoids refetching, exact backend sort order may drift after local insertion until the folder is naturally refetched.
|
||||
|
||||
## Stores
|
||||
|
||||
All in `data/stores/`:
|
||||
|
||||
| Store | Owns |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| `EntriesStore` | Index of loaded entries by path, child/selected/active lookup, local remove and rename. Never fetches. |
|
||||
| `ExplorerStore` | Active path, selected paths, expanded tree paths, breadcrumbs, filter, sort. |
|
||||
| `DriveDialogStore` | Create-directory and rename dialogs, entry selected for rename. |
|
||||
| `UploadOperationStore` | Upload queue, progress, retry/remove/abort, operation card visibility. |
|
||||
| `DeleteOperationStore` | Delete queue, progress, retry/remove, operation card visibility. |
|
||||
| `SettingsStore` | Persisted view mode, compact mode, sidebar visibility, grid size. |
|
||||
|
||||
Operation stores receive the React Query mutation function from the view layer and manage only operation state.
|
||||
|
||||
## Providers
|
||||
|
||||
`DriveProvider` creates and exposes the feature stores — `const { entriesStore, explorerStore } = useDrive();`. `SettingsProvider` owns persisted settings.
|
||||
|
||||
## Pages and Widgets
|
||||
|
||||
- `DriveExplorerPage` — main browser: `DriveProvider`, toolbar, content, sidebar, footer, operation watcher.
|
||||
- `DriveViewerPage` — details and preview, via `useGetFile()`.
|
||||
- `DriveSettingsPage` — persisted UI preferences.
|
||||
- `ImageEditorPage` / `ImageMapPage` — use `useGetFile()` + `useGetRawFile()` (editor also `useUploadFile()`). Config and helpers live in `view/widgets/ImageEditor/` so `view` code never imports from `pages`.
|
||||
- `FilePickerDialog` — reusable picker widgets in `view/widgets/FilePickerDialog/`; keep picker behavior feature-scoped and pass reusable UI state through props/context instead of coupling low-level components to page-level stores.
|
||||
|
||||
## View Layer Rules
|
||||
|
||||
Feature widgets may use `useDrive()` and `useSettings()`. Lower-level reusable components (`ExplorerFileEntryGrid`, `FileManagerFileIcon`, `FilePreview`, …) stay prop-driven. Never import backend DTOs into view components — views consume frontend entities and props.
|
||||
|
||||
## File Types and Icons
|
||||
|
||||
Helpers in `shared/file/`; icons in `apps/admin/public/media/files/`. To add an icon: add the asset there, update `shared/file/fileTypeImportMap.ts`, and confirm `FileManagerFileIcon` falls back correctly for unknown extensions.
|
||||
|
||||
## Adding a Query or Mutation
|
||||
|
||||
1. Add/reuse a method in `data/api/api.ts`.
|
||||
2. Query: add a key in `data/queries/keys.ts` and a query file in `data/queries/`, mapping DTOs to entities before returning. Mutation: add a mutation hook in `data/queries`, call `mutateAsync` from the consuming widget or operation store.
|
||||
3. Update `EntriesStore` if lookup state changes; update `data/queries/cache.ts` if the visible cache must change.
|
||||
4. Never import MobX stores or providers inside `data/queries`. Don't invalidate entries queries per operation unless a full refetch is intentional.
|
||||
|
||||
## Frontend Change Checklist
|
||||
|
||||
1. Is the data server state or local UI state?
|
||||
2. Backend calls stay in `data/api`; React Query hooks stay free of MobX provider imports.
|
||||
3. Map DTOs before view components.
|
||||
4. Update cache helpers when mutation results should appear immediately.
|
||||
5. Update backend docs if the change requires a backend contract change.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Permission Module
|
||||
|
||||
The Permission module provides frontend permission checks and gates. It also corresponds to the backend permission endpoint and permission discovery service used by AccessControl.
|
||||
|
||||
## Source Map
|
||||
|
||||
| Area | Path |
|
||||
| -------------------------- | -------------------------------------------------------- |
|
||||
| Frontend module | `apps/admin/src/core/Permission/` |
|
||||
| Frontend gates | `apps/admin/src/core/Permission/view/gates/` |
|
||||
| Frontend hook | `apps/admin/src/core/Permission/hooks/usePermissions.ts` |
|
||||
| Frontend types | `apps/admin/src/core/Permission/entities/permission.ts` |
|
||||
| Backend module | `apps/backend/app/base_modules/Permission/` |
|
||||
| Backend permission service | `apps/backend/app/services/permission_service.ts` |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------- | ------------------------------------------------------------------ |
|
||||
| [Frontend](./frontend.md) | Permission types, hook, gates, and usage rules. |
|
||||
| [Backend](./backend.md) | Permission endpoint, permission discovery, and permission configs. |
|
||||
|
||||
## Main Capabilities
|
||||
|
||||
- Check whether current user has required permission slugs.
|
||||
- Support `all` and `any` matching modes.
|
||||
- Render or hide UI actions through `PermissionGate`.
|
||||
- Protect full pages through `PermissionPageGate`.
|
||||
- Treat super admin as an all-permissions user.
|
||||
|
||||
## High-Level Flow
|
||||
|
||||
```txt
|
||||
AuthProvider current user
|
||||
-> AuthUser.permissions
|
||||
-> usePermissions()
|
||||
-> hasPermission()
|
||||
-> PermissionGate or PermissionPageGate
|
||||
```
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- Permission frontend owns permission checks and gate components.
|
||||
- Auth owns loading current user permissions.
|
||||
- AccessControl owns managing roles and assigning permissions.
|
||||
- Backend modules own their own permission definitions in `config/permissions.ts`.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Permission Backend
|
||||
|
||||
The backend Permission module exposes available permission definitions. It is used by AccessControl when assigning permissions to roles.
|
||||
|
||||
Source: `apps/backend/app/base_modules/Permission/`
|
||||
|
||||
## Route
|
||||
|
||||
| Method | Route | Controller Method | Auth Required | Purpose |
|
||||
| ------ | ------------- | ----------------- | ------------- | -------------------------------------------- |
|
||||
| `GET` | `/permission` | `index` | Yes | Return all available permission definitions. |
|
||||
|
||||
Route file:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Permission/routes.ts
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
```txt
|
||||
GET /permission
|
||||
-> auth middleware
|
||||
-> PermissionController.index
|
||||
-> getAllPermissions()
|
||||
-> scan backend module permission configs
|
||||
-> return PermissionItem[]
|
||||
```
|
||||
|
||||
## Permission Discovery
|
||||
|
||||
Permission discovery lives in:
|
||||
|
||||
```txt
|
||||
apps/backend/app/services/permission_service.ts
|
||||
```
|
||||
|
||||
`getAllPermissions()` scans:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/*/config/permissions.ts
|
||||
apps/backend/app/modules/*/config/permissions.ts
|
||||
```
|
||||
|
||||
Each config exports permission items:
|
||||
|
||||
```ts
|
||||
type PermissionItem = {
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
||||
## Current Permission Configs
|
||||
|
||||
Core permission definitions currently include:
|
||||
|
||||
```txt
|
||||
apps/backend/app/base_modules/Admin/config/permissions.ts
|
||||
apps/backend/app/base_modules/Role/config/permissions.ts
|
||||
apps/backend/app/base_modules/FileManager/config/permissions.ts
|
||||
```
|
||||
|
||||
When adding a feature that needs access control, add permission definitions to the owning backend module's `config/permissions.ts`.
|
||||
|
||||
## Admin Permission Slugs
|
||||
|
||||
`getAdminPermissionSlugs(admin)` loads an admin's roles and returns a set of permission slugs from `role_permissions`.
|
||||
|
||||
This function is used by backend policies to authorize operations.
|
||||
|
||||
## Ownership Rules
|
||||
|
||||
- Backend modules own their own permission definitions.
|
||||
- Permission service owns discovering all available permission definitions.
|
||||
- AccessControl owns assigning permission slugs to roles.
|
||||
- Auth owns returning the current user's permission slugs to the frontend.
|
||||
@@ -0,0 +1,98 @@
|
||||
# Permission Frontend
|
||||
|
||||
The frontend module provides reusable permission-checking primitives for the admin app.
|
||||
|
||||
Source: `apps/admin/src/core/Permission/`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```txt
|
||||
Permission/
|
||||
config/ Static module config and messages.
|
||||
entities/ Permission types.
|
||||
hooks/ Current-user permission hook.
|
||||
lib/ Pure permission check function.
|
||||
view/gates/ React gate components.
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
Permission types live in:
|
||||
|
||||
```txt
|
||||
entities/permission.ts
|
||||
```
|
||||
|
||||
Key concepts:
|
||||
|
||||
```ts
|
||||
type PermissionCheckMode = "all" | "any";
|
||||
type RequiredPermissions = string | string[];
|
||||
```
|
||||
|
||||
## Hook
|
||||
|
||||
Current-user permission access:
|
||||
|
||||
```txt
|
||||
hooks/usePermissions.ts
|
||||
```
|
||||
|
||||
The hook reads auth state from AuthProvider and returns:
|
||||
|
||||
```ts
|
||||
permissions;
|
||||
isAdmin;
|
||||
loading;
|
||||
```
|
||||
|
||||
`isAdmin` is based on the configured super admin id.
|
||||
|
||||
## Pure Permission Check
|
||||
|
||||
Permission check logic:
|
||||
|
||||
```txt
|
||||
lib/hasPermission.ts
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
- Super admin passes all checks.
|
||||
- `all` mode requires every permission.
|
||||
- `any` mode requires at least one permission.
|
||||
- Empty required permissions should be treated carefully at call sites.
|
||||
|
||||
## Gates
|
||||
|
||||
### `PermissionGate`
|
||||
|
||||
Use for conditional UI controls such as buttons and menu items.
|
||||
|
||||
```tsx
|
||||
<PermissionGate requiredPermissions="admins.update">
|
||||
<Button>Edit</Button>
|
||||
</PermissionGate>
|
||||
```
|
||||
|
||||
### `PermissionPageGate`
|
||||
|
||||
Use for route-level access protection.
|
||||
|
||||
```tsx
|
||||
<PermissionPageGate requiredPermissions={["admins.view", "role.view"]}>
|
||||
<AdminListPage />
|
||||
</PermissionPageGate>
|
||||
```
|
||||
|
||||
If access is denied, it renders the provided fallback or a 403 page.
|
||||
|
||||
## Translations
|
||||
|
||||
The module has a local message bundle for consistency:
|
||||
|
||||
```txt
|
||||
config/messages.ts
|
||||
config/translation/en.json
|
||||
config/translation/uk.json
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Web Manager
|
||||
|
||||
The CMS behind the public website. Editors assemble localized pages from ready-made blocks in the
|
||||
admin panel; the public Next.js site renders what they publish.
|
||||
|
||||
| Part | Source | Owns |
|
||||
| ---------------------- | ------------------------------------------- | -------------------------------------------------------- |
|
||||
| Backend (`WebManager`) | `apps/backend/app/base_modules/WebManager/` | Data, validation, draft/publish transactions, APIs |
|
||||
| Admin (`Web Studio`) | `apps/admin/src/core/WebStudio/` | Editor UI, drafts, publishing actions, preview, settings |
|
||||
| Public site | `apps/web/web-runtime/`, `apps/web/app/` | Server rendering, layouts, blocks, caching, SEO |
|
||||
|
||||
## Documentation
|
||||
|
||||
| Page | Purpose |
|
||||
| ------------------------------- | ------------------------------------------------------------------ |
|
||||
| [Backend](./backend.md) | Data model, HTTP API, query budget, tuning constants. |
|
||||
| [Admin](./admin.md) | Web Studio: routes, editor, config files, UI conventions. |
|
||||
| [Web](./web.md) | Public site rendering, layouts, hero, SEO, degradation. |
|
||||
| [Integration](./integration.md) | Contracts, cross-app flows, caching, env wiring, extending, tests. |
|
||||
|
||||
## For Editors
|
||||
|
||||
A page is a title, an optional hero, an ordered list of blocks, and SEO fields. Blocks are the
|
||||
prebuilt sections — text columns, FAQ, media, banner, article list — so building a page is picking
|
||||
blocks and filling them in, not writing markup.
|
||||
|
||||
| Term | What it means |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Page** | One address in one language, e.g. `/en/about-us`. |
|
||||
| **Group key** | Ties the same page across languages together (`about-us` links `/en/about-us` and `/uk/pro-nas`) so the site can offer a language switch. |
|
||||
| **Page type** | Landing, Blog, or Blog item. Decides the layout and which blocks are offered. |
|
||||
| **Hero** | The opening section. Four intensities: none, low, middle, high. |
|
||||
| **Draft** | Your working copy. Never visible to visitors. |
|
||||
| **Published** | Live on the site. |
|
||||
| **Unpublished** | Taken down, but kept — content, blocks and address stay reserved, so you can put it back. |
|
||||
| **Preview** | A private, time-limited link that renders the draft exactly as the site would. |
|
||||
| **Site settings** | Header menus, footer, and legal links — shared by every page in one language. |
|
||||
|
||||
Everything you edit is a draft. A live page keeps serving the old version until you press Publish,
|
||||
so an unfinished edit can never reach visitors. Nothing is deleted unless you press Delete.
|
||||
|
||||
## Page Lifecycle
|
||||
|
||||
```txt
|
||||
create publish
|
||||
(none) ────────► draft ───────────► published ◄──────┐
|
||||
│ │ │
|
||||
discard│ │unpublish │publish
|
||||
▼ ▼ │
|
||||
deleted unpublished ───────┘
|
||||
|
||||
published + open draft clone: visitors see the published row, the editor edits the clone
|
||||
```
|
||||
|
||||
Every write happens in one transaction with rows locked `FOR UPDATE`; routes and permissions are in [backend.md](./backend.md), purge behavior in [integration.md](./integration.md#caching-and-revalidation).
|
||||
|
||||
- **Create** — `createDraft` validates the payload, asserts the `locale` + `path` is free across every root row (any status), and inserts the page with `status = draft` plus its components. Nothing public yet.
|
||||
- **Save a draft** — `ensureDraft` edits a `draft` or `unpublished` page in place; for a `published` page it reuses or creates the draft clone. Components are replaced wholesale in the submitted order; a clone inherits its original's address.
|
||||
- **Edit a published page** — the clone is a second row with `draft_of_id` pointing at the original (one clone per page, enforced by a partial unique index). Edit always opens the clone; the original keeps serving until the clone is published.
|
||||
- **Preview** — a signed token (HMAC over `APP_KEY`, TTL 900s clamped to 60–3600s) lets the web app render the draft in an iframe sheet: `force-dynamic`, `noindex`, disallowed in robots. Absent, tampered, or expired tokens render "preview unavailable".
|
||||
- **Publish** — publishes the **last saved** draft (the confirmation dialog says so). A pending clone is always preferred, so publishing can never drop the editor's work: a root draft goes live directly; a clone is folded into its original and deleted. A _structural_ change (first publish, republish, or changed locale/path/title/group key/page type) purges the whole cache tag; anything else purges only the affected page tags.
|
||||
- **Unpublish** — same permission as publish; only valid on a published page (else 409). Sets `status = unpublished`, keeping content, components, and any open clone; the address stays reserved. Full purge — the URL now 404s.
|
||||
- **Republish** — publish again; an unpublished page is edited in place, and republishing counts as structural.
|
||||
- **Discard a draft** — deletes the draft row and its components; a clone's original is untouched. No purge.
|
||||
- **Delete** — the only destructive action, behind a confirmation dialog. Components and any open clone cascade with the row; full purge if it was published.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Web Manager — Admin (Web Studio)
|
||||
|
||||
Admin side of [Web Manager](./Readme.md). Source: `apps/admin/src/core/WebStudio/`.
|
||||
|
||||
## Routes
|
||||
|
||||
```txt
|
||||
/web-studio/pages/listing list, search, filters, row actions
|
||||
/web-studio/pages/-/create new draft
|
||||
/web-studio/pages/-/:id editor (page or draft clone)
|
||||
/web-studio/settings/:section header | footer | legal
|
||||
```
|
||||
|
||||
The list shows title with full path, locale, slug, group key, page type, status, draft marker and
|
||||
updated time; search is debounced and filters, pagination and locale live in the query string
|
||||
(`useWebStudioQueryParams`), so links and the back arrow keep the editor's context. Row actions are
|
||||
Edit, Preview, Open in web app, Publish, Unpublish, Discard and Delete — Edit opens the draft clone
|
||||
when `draftId` is present, so an editor always lands on the working copy.
|
||||
|
||||
## Editor
|
||||
|
||||
The editor is a flat two-column layout: a sticky ~320px Page details column, and Hero / Blocks / SEO
|
||||
as line tabs beside it. One React Hook Form instance (`PAGE_EDITOR_FORM_ID`) owns everything;
|
||||
the header's Save button submits it by form id, and submitting disables the whole fieldset.
|
||||
Actions sit in a split button: Save draft, then Preview / Publish / Unpublish / Discard / Delete in
|
||||
the dropdown, each behind its own control component in `view/controls/` and its own permission.
|
||||
Publish is offered whenever the page is not already live, so it doubles as republish.
|
||||
|
||||
Blocks are added from a sheet filtered to the page type's `allowedBlockTypes`, reordered by drag
|
||||
(`view/components/SortableList.tsx`), and duplicated or removed from their own header. Nested
|
||||
collections — content columns, CTAs, FAQ items, navigation links — behave the same way one level
|
||||
down.
|
||||
|
||||
## Key Files
|
||||
|
||||
| Concern | File |
|
||||
| ------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| Module registration | `config/module.ts`, `config/routing.tsx`, `config/menu.ts` |
|
||||
| Languages | `config/languages.ts` — code, labels, direction, public prefix |
|
||||
| Page types | `config/pages.ts` — allowed blocks and default path per type |
|
||||
| Block catalogue | `config/blocks.ts` — label, description, preview image, factory |
|
||||
| Settings shape | `config/siteSettings.ts` |
|
||||
| HTTP + multipart | `data/api/WebStudioAPI.ts`, `data/api/PagePayloadSerializer.ts` |
|
||||
| Domain types | `entities/` — `PageType`, `PageStatus`, `BlockType`, `HeroType`, block and field shapes |
|
||||
| Form ↔ DTO | `pages/PageEditPage/helpers/toEditorValues.ts`, `view/forms/PageEditorForm/helpers/toCreatePayload.ts` |
|
||||
|
||||
Page types decide what an editor can build:
|
||||
|
||||
| Type | Layout | Blocks offered | Default path |
|
||||
| ----------- | ---------- | --------------------------------------------------- | ------------ |
|
||||
| `landing` | Standard | all six | `/` |
|
||||
| `blog` | Blog index | none — the listing is rendered from published posts | `/blog` |
|
||||
| `blog-item` | Editorial | all except `articles` | `/` |
|
||||
|
||||
## UI Conventions
|
||||
|
||||
The module is a content tool, not a dashboard of cards. Flat surfaces, few separators, one primary
|
||||
action per screen.
|
||||
|
||||
| Element | Treatment |
|
||||
| ------------------- | -------------------------------------------------------- |
|
||||
| Screen heading | `text-mono text-xl font-semibold` |
|
||||
| Supporting copy | `text-muted-foreground text-sm` |
|
||||
| Editor settings | `bg-muted/40 space-y-5 p-5` |
|
||||
| Top-level block | `bg-muted/35`, compact header, one `border-b` while open |
|
||||
| Nested field group | `border-l-2 pl-4` (`view/components/FieldCard.tsx`) |
|
||||
| Inline actions | small ghost icon buttons |
|
||||
| Destructive trigger | ghost button, destructive text, confirmation dialog |
|
||||
| Empty collection | `bg-muted/35`, generous padding, short copy |
|
||||
|
||||
`view/components/BlockCard.tsx` is named for compatibility but is a flat section, not a card — no
|
||||
shadow, no hover lift, no bordered container nested inside every block. The block picker stays a
|
||||
sheet with a divided list, because picking is a temporary task. Shared visual principles live in the
|
||||
[Admin UI Style Guide](../../../apps/admin/ui-style-guide.md); style Web Studio from Web Studio and do
|
||||
not edit `apps/admin/src/shared` for a module-only visual change.
|
||||
|
||||
A visual change must not alter routes, `locale` / `group_key` query behavior, list filtering or DTO
|
||||
mapping, draft-clone semantics, form values or validation, block factory output, settings payload
|
||||
shape, or the toast / error / loading states. Anything that does is a feature change.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Web Manager — Backend
|
||||
|
||||
Backend of [Web Manager](./Readme.md). Source: `apps/backend/app/base_modules/WebManager/`.
|
||||
Lifecycle mechanics: [Page Lifecycle](./Readme.md#page-lifecycle). Cross-app flows: [Integration](./integration.md).
|
||||
|
||||
## Data Model
|
||||
|
||||
### `web_pages`
|
||||
|
||||
```txt
|
||||
id
|
||||
status -- draft | published | unpublished
|
||||
draft_of_id -- nullable FK to web_pages, ON DELETE CASCADE
|
||||
locale -- en, uk, de
|
||||
path -- '' for home, else 'about-us' or 'blog/post/about-me' (no leading slash)
|
||||
group_key -- links localized versions of the same page
|
||||
page_type -- landing | blog | blog-item
|
||||
title
|
||||
hero -- jsonb, nullable
|
||||
seo_title -- defaults to ''
|
||||
seo_description
|
||||
published_at -- nullable
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
Indexes: `(group_key, status)`, `(locale, path, status)`, a partial unique index on
|
||||
`(locale, path) WHERE status = 'published'`, and a partial unique index on
|
||||
`(draft_of_id) WHERE draft_of_id IS NOT NULL` — one draft clone per page, enforced by the database.
|
||||
|
||||
Rules:
|
||||
|
||||
- `path` is the full route after the locale, without leading or trailing slashes: `/en/blog/post/x`
|
||||
is `locale = en`, `path = blog/post/x`. The editor form shows it with a leading slash.
|
||||
- Public reads only ever match `status = published`.
|
||||
- A root row is `draft_of_id = null`; a draft clone points at its published original.
|
||||
- Route uniqueness is enforced twice: the partial index for live collisions, and
|
||||
`assertNewRouteAvailable` for root rows of any status, so an unpublished page keeps its address.
|
||||
|
||||
### `web_components`
|
||||
|
||||
```txt
|
||||
id
|
||||
page_id -- FK to web_pages, ON DELETE CASCADE
|
||||
type -- content | faq | media-content | articles | banner | media
|
||||
data -- jsonb, block-specific shape validated by validator/web_content_validator.ts
|
||||
sort_order -- position in the page, index of the submitted array
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
Indexed on `(page_id, sort_order)`. Saving a draft deletes and reinserts the whole set rather than
|
||||
diffing it.
|
||||
|
||||
### `web_settings`
|
||||
|
||||
```txt
|
||||
id
|
||||
locale -- unique
|
||||
header -- jsonb { menus: [{ title, items }], ctaItems }
|
||||
footer -- jsonb { copyright, menus }
|
||||
legal -- jsonb { privacyPath, termsPath }
|
||||
created_at / updated_at
|
||||
```
|
||||
|
||||
One row per locale, upserted by locale. Shape lives in `WebStudio/config/siteSettings.ts`.
|
||||
|
||||
## HTTP API
|
||||
|
||||
Admin routes are under `/web-manager` behind `middleware.auth()`; every action authorizes through
|
||||
`WebManagerPolicy`.
|
||||
|
||||
| Method | Route | Permission | Purpose |
|
||||
| ------ | -------------------------------------- | ---------------------- | ------------------------------------------- |
|
||||
| GET | `/web-manager/pages` | `web_manager.view` | Paginated root rows with a `draft` relation |
|
||||
| GET | `/web-manager/pages/group-keys` | `web_manager.view` | Distinct group keys, alphabetized |
|
||||
| POST | `/web-manager/pages` | `web_manager.create` | Create a draft (multipart) |
|
||||
| GET | `/web-manager/pages/:id` | `web_manager.view` | Page + components |
|
||||
| PUT | `/web-manager/pages/:id/draft` | `web_manager.update` | Save a draft (multipart) |
|
||||
| POST | `/web-manager/pages/:id/publish` | `web_manager.publish` | Publish, returns `meta.revalidation` |
|
||||
| POST | `/web-manager/pages/:id/unpublish` | `web_manager.publish` | Take down, returns `meta.revalidation` |
|
||||
| POST | `/web-manager/pages/:id/preview-token` | `web_manager.view` | Signed preview token |
|
||||
| DELETE | `/web-manager/pages/:id` | `web_manager.delete` | Delete page or discard draft |
|
||||
| GET | `/web-manager/settings` | `web_manager.view` | Settings, all locales or one |
|
||||
| PUT | `/web-manager/settings` | `web_manager.settings` | Upsert settings for a locale |
|
||||
|
||||
Public routes are under `/web-manager/public`, unauthenticated, and return published content only —
|
||||
except `preview`, which is gated by the signed token.
|
||||
|
||||
| Route | Returns |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------- |
|
||||
| `/pages/resolve?locale=&path=` | Page, components, settings, breadcrumbs, `languageAlternatives` |
|
||||
| `/pages/preview?token=` | The same shape for any status, for a valid token |
|
||||
| `/pages/sitemap?page=&perPage=` | `locale`, `path`, `groupKey`, `pageType`, `publishedAt`, `updatedAt` + `pagination` |
|
||||
| `/blog/pages?locale=&page=&perPage=` | Published `blog-item` pages, newest first, with hero media |
|
||||
| `/settings?locale=` | Header, footer, legal for one locale |
|
||||
|
||||
The sitemap is the one public read proportional to the site, so it is paginated
|
||||
(`SITEMAP_PAGE_SIZE` 1000, capped at `SITEMAP_MAX_PAGE_SIZE` 5000) and the caller walks it;
|
||||
`groupKey` is included so hreflang alternates need no second lookup. The resolve payload carries
|
||||
`publishedAt` / `updatedAt`, which the web app emits as `datePublished` / `dateModified`.
|
||||
|
||||
Permissions are seeded from `WebManager/config/permissions.ts`: `web_manager.view`, `.create`,
|
||||
`.update`, `.publish`, `.delete`, `.settings`.
|
||||
|
||||
## Public Resolve Query Budget
|
||||
|
||||
| Query | Bounded by |
|
||||
| --------------------- | ----------------------------------------------------------- |
|
||||
| the page row | route lookup |
|
||||
| its components | blocks on the page |
|
||||
| site settings | one row per locale |
|
||||
| language alternatives | locales sharing the `group_key` |
|
||||
| breadcrumbs | path depth (~4–6 rows), plus one query per group-key jump |
|
||||
| internal page links | internal links in the page's own content, skipped when none |
|
||||
|
||||
Two details keep that true. **Link map by id**: `collectInternalPageIds` walks hero, components and
|
||||
settings first, then `createPageLinkMap` fetches only those ids — unknown or unpublished ids are
|
||||
absent and render as an empty href. **Breadcrumbs by candidate**: the walk can only climb to a
|
||||
shorter prefix of the current path, the home page, or the group key in
|
||||
`BREADCRUMB_PAGE_TYPE_PARENTS`, so `buildBreadcrumbs` asks for precisely those rows; a second query
|
||||
happens only when a group-key jump lands on a branch whose ancestors were not already loaded
|
||||
(`ancestorsAlreadyLoaded`). Both are served by the existing indexes.
|
||||
|
||||
## Tuning Constants
|
||||
|
||||
Backend tuning constants live in `WebManager/config/general.ts`: media upload path and 10 MB size
|
||||
limit, preview token TTLs, sitemap page sizes, and `BREADCRUMB_PAGE_TYPE_PARENTS` (which makes
|
||||
`blog` the breadcrumb parent of every `blog-item`).
|
||||
@@ -0,0 +1,185 @@
|
||||
# Web Manager — Integration
|
||||
|
||||
How the three apps of [Web Manager](./Readme.md) connect: contracts, flows, caching, env wiring, and cross-app extension checklists.
|
||||
|
||||
## How the Three Apps Connect
|
||||
|
||||
```txt
|
||||
┌───────────────────────┐ ┌──────────────────────────┐
|
||||
│ Admin (Web Studio) │───────►│ Backend (AdonisJS) │
|
||||
│ editor, drafts, │◄───────│ pages, components, │
|
||||
│ publish, preview │ DTOs │ settings, transactions │
|
||||
└───────────┬───────────┘ └────────────┬─────────────┘
|
||||
│ │
|
||||
│ iframe /preview?token=… │ POST /api/revalidate (tag purge)
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Public site (Next.js) │
|
||||
│ server-renders GET /web-manager/public/*, tagged 60s cache │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
The three never share code, only contracts:
|
||||
|
||||
| Contract | Defined by | Consumed by |
|
||||
| --------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
|
||||
| Page / block payloads | `types/web_content.ts`, `validator/web_content_validator.ts` | Admin DTOs, `web-runtime/types.ts` |
|
||||
| Public read API | `WebManager/routes.ts` (`/web-manager/public/*`) | `web-runtime/api/web-runtime-api.ts` |
|
||||
| Cache tag names | `services/web_revalidation_service.ts` | `web-runtime/api/cache-tags.ts` — **must stay in sync** |
|
||||
| Preview token | `services/preview_token_service.ts` | Admin preview sheet, `app/[locale]/preview/` |
|
||||
| Block / page types | `types/web_content.ts` | `WebStudio/config/blocks.ts`, `web-runtime/*/registry` |
|
||||
|
||||
## Save Flow (admin → backend)
|
||||
|
||||
```txt
|
||||
editor submits React Hook Form values
|
||||
PagePayloadSerializer → FormData: `payload` JSON string + one binary part per new file
|
||||
POST /web-manager/pages | PUT /web-manager/pages/:id/draft (multipart/form-data)
|
||||
PagePayloadService parses `payload`, validates, stores files through FileManager,
|
||||
rewrites { name, fileKey } media values into { name, ref }
|
||||
WebManagerService writes page + components in one transaction
|
||||
```
|
||||
|
||||
Media inside `hero` and block `data` is either `{ name, ref }` (already stored) or
|
||||
`{ name, fileKey }` (new upload); only `ref` values are ever persisted. Part names are the
|
||||
`fileKey`s (`file-0`, `file-1`, …). A request without a `payload` part is read as a plain JSON body,
|
||||
so the endpoints stay usable from tests and scripts. The payload travels as one JSON string so
|
||||
booleans, numbers, nulls and empty arrays survive the multipart transport.
|
||||
|
||||
## Publish Flow (backend → public site)
|
||||
|
||||
```txt
|
||||
publish/unpublish/settings commit
|
||||
WebRevalidationService POSTs { tags } to WEB_APP_URL/api/revalidate with X-Revalidate-Secret
|
||||
Next.js revalidateTag(tag, 'max') per tag
|
||||
response meta.revalidation = purged | retrying | disabled → admin toast
|
||||
```
|
||||
|
||||
## Public Request Flow (visitor → web → backend)
|
||||
|
||||
```txt
|
||||
GET /en/about-us
|
||||
next-intl middleware resolves the locale segment
|
||||
app/[locale]/[[...path]]/page.tsx → resolvePageRenderData(locale, path)
|
||||
GET /web-manager/public/pages/resolve?locale=en&path=about-us (next: revalidate 60 + tags)
|
||||
serializePublicPage returns page, components, settings, breadcrumbs, languageAlternatives
|
||||
renderer.tsx → header + RegisteredPageLayout + footer; blocks via the block registry
|
||||
```
|
||||
|
||||
Before it leaves the backend, `PublicContentService.prepare` resolves internal links to hrefs,
|
||||
turns stored file refs into URLs, and sanitizes rich text (`content_sanitizer.ts`: allowlisted tags,
|
||||
`http/https/mailto/tel` only, `rel` limited to `nofollow`/`noopener`/`noreferrer`). The web app
|
||||
renders rich text as HTML and trusts that sanitization.
|
||||
|
||||
## Caching and Revalidation
|
||||
|
||||
Every public fetch is tagged and time-boxed, so a purge that never lands costs about a minute of
|
||||
stale content rather than correctness.
|
||||
|
||||
| Fetch | `revalidate` | Tags |
|
||||
| --------- | ------------ | --------------------------------------------------------------- |
|
||||
| page | 60s | `web-manager`, `web-manager:page:{locale}:{path}`, settings tag |
|
||||
| settings | 60s | `web-manager`, `web-manager:settings:{locale}` |
|
||||
| blog list | 60s | `web-manager`, `web-manager:blog:{locale}` |
|
||||
| sitemap | 3600s | `web-manager` only — it spans every locale and page |
|
||||
|
||||
Tag builders are duplicated in `web_revalidation_service.ts` (backend) and `cache-tags.ts` (web) and
|
||||
must stay in sync; both sides have tests over the names, because a drift stops purges from landing
|
||||
without anything failing.
|
||||
|
||||
Delivery is one attempt inline with the request (so the outcome can be reported to the editor) and
|
||||
attempts 2–3 in the background with exponential backoff and jitter. Publish, unpublish and settings
|
||||
save therefore return `meta.revalidation`:
|
||||
|
||||
| `status` | Meaning |
|
||||
| ---------- | ----------------------------------------------------------------------- |
|
||||
| `purged` | The web app confirmed the purge. |
|
||||
| `retrying` | The first attempt failed; background retries are running. |
|
||||
| `disabled` | `WEB_APP_URL` or `WEB_REVALIDATE_SECRET` is unset, so nothing was sent. |
|
||||
|
||||
The admin shows a warning toast on anything but `purged`, so an editor is never told the site is up
|
||||
to date when it is not. Retries live in memory only — a restart drops them onto the same 60s floor a
|
||||
failed purge already has. Exhausting all attempts logs at `error`; publishing again sends a fresh
|
||||
purge. A deployment missing either variable logs each skipped purge at `warn` in production.
|
||||
|
||||
## Configuration
|
||||
|
||||
| App | Variable | Purpose |
|
||||
| ------- | ----------------------- | ----------------------------------------------------------------------------- |
|
||||
| backend | `APP_KEY` | Signs preview tokens. |
|
||||
| backend | `WEB_APP_URL` | Where to POST cache purges. Server-side URL (compose service name in Docker). |
|
||||
| backend | `WEB_REVALIDATE_SECRET` | Must equal the web app's `REVALIDATE_SECRET`. |
|
||||
| web | `BACKEND_API_URL` | Backend base URL, without the `/web-manager/public/...` part. Required. |
|
||||
| web | `REVALIDATE_SECRET` | Guards `/api/revalidate`. |
|
||||
| web | `SITE_URL` | Canonical origin for metadata. Required in production. |
|
||||
| admin | `VITE_WEB_URL` | Public site origin used to build preview and "view page" links. |
|
||||
|
||||
Backend tuning constants live in `WebManager/config/general.ts` — see
|
||||
[Backend → Tuning Constants](./backend.md#tuning-constants).
|
||||
|
||||
## Extending
|
||||
|
||||
### Add a block type
|
||||
|
||||
1. Backend: add the value to `BLOCK_TYPES` and its data type in `WebManager/types/web_content.ts`,
|
||||
and its shape to the component union in `validator/web_content_validator.ts`, so the payload is
|
||||
validated server-side.
|
||||
2. Admin: add the enum value to `entities/BlockType.ts`, the block shape under `entities/block/`,
|
||||
an editor under `view/forms/blocks/<Name>Block/` with its `schema.ts`, and an entry in
|
||||
`config/blocks.ts` (label, description, preview image, `createBlock` factory). Add it to the
|
||||
`allowedBlockTypes` of every page type that should offer it in `config/pages.ts`.
|
||||
3. Web: add the renderer under `web-runtime/blocks/components/` and register it in
|
||||
`web-runtime/blocks/registry.tsx`, plus the type in `web-runtime/types.ts`.
|
||||
4. Translations: add the label and description keys to both `config/translation/en.json` and
|
||||
`uk.json` — the two files are kept at exact key parity.
|
||||
|
||||
`apps/web/tests/registry.test.ts` keeps a hand-mirrored copy of `BLOCK_TYPES` and `PAGE_TYPES` and
|
||||
asserts the registries match it exactly — add the type there too, and it becomes the tripwire for a
|
||||
block the backend can publish but the site cannot draw.
|
||||
|
||||
### Block preview images
|
||||
|
||||
Wireframe SVGs in `apps/admin/public/media/web-studio/blocks/<type>.svg`, referenced by
|
||||
`previewImage` in `config/blocks.ts` and rendered by `BlockPickerSheet` as a 4:3 thumbnail. They are
|
||||
schematic, not screenshots: `viewBox="0 0 400 300"`, no `width`/`height`, content inside
|
||||
`x: 40–360` / `y: 32–268`, and no text — everything reads as bars and shapes at 112px wide.
|
||||
|
||||
| Token | Hex | Used for |
|
||||
| -------------- | --------- | --------------------------------------------- |
|
||||
| background | `#f7f4ec` | full-canvas rect, always the first element |
|
||||
| surface | `#fffdf8` | cards, accordion rows, banner strip |
|
||||
| surface border | `#d9dacf` | 1px stroke on surfaces and image placeholders |
|
||||
| image fill | `#e7e9dc` | image placeholder rects |
|
||||
| ink | `#1f291f` | titles, button inner bars, icons |
|
||||
| text line | `#ccd1c0` | body copy bars |
|
||||
| muted line | `#b9bfab` | subtitles, captions, image glyphs |
|
||||
| accent | `#c7ff3f` | primary CTA pills, sun circle, active state |
|
||||
| accent border | `#a8c930` | stroke marking an active or open element |
|
||||
|
||||
Reuse the shared vocabulary so previews read as one family: title bar (dark rect, `height 14–18`,
|
||||
`rx = height/2`), body line (`height 9–10`, last line ~70% width), card (surface + border,
|
||||
`rx 10–14`), CTA pill (accent, `height 24–28`, dark label bar inside), image placeholder (mountain
|
||||
polygon plus accent sun). Draw the distinguishing structure only — three cards means "a list",
|
||||
2–4 lines mean "a paragraph" — and keep at most one accent element per preview.
|
||||
|
||||
### Add a page type
|
||||
|
||||
Add the value to `PAGE_TYPES` in `WebManager/types/web_content.ts` and `entities/PageType.ts`, a
|
||||
`WebStudioPageConfig` in `config/pages.ts` (allowed blocks, default path, label keys), a layout
|
||||
component in `web-runtime/layouts/components/`, its entry in `PAGE_LAYOUT_REGISTRY`, and the
|
||||
mirrored list in `registry.test.ts`. An unregistered type still renders through the standard
|
||||
layout, so the site cannot break mid-rollout.
|
||||
|
||||
## Tests
|
||||
|
||||
| Suite | Command | Location | Covers |
|
||||
| ------- | ---------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Backend | `npm run test:backend` | `apps/backend/tests/functional/web_manager/` | Preview token, revalidation, public content, sanitizer, breadcrumbs; page lifecycle (`web_manager_service.spec.ts`: draft/clone/publish/unpublish, route reservation, purge tags); HTTP surface (`pages_api.spec.ts`: 401/403 per permission, multipart, public endpoints). |
|
||||
| Admin | `npm run test:admin` | `apps/admin/tests/unit/core/WebStudio/` | Menu, SEO helpers, list mapper, payload serializer, editor-value mapping and validation, `toCreatePayload`, block factory↔schema round-trips, field schemas, page paths, permission helper. |
|
||||
| Web | `npm run test:web` | `apps/web/tests/` | Normalization, cache tags, block/layout registries, page metadata, sitemap, site URL, structured data, `safeHref`, published-only gate, `/api/revalidate`, API client, blog pager. |
|
||||
|
||||
Notes:
|
||||
|
||||
- DB-backed backend specs run against a dedicated `adonis_test` database (`.env.test`): the runner migrates up/down and each test runs in a global transaction (fixtures in `tests/utils/web_manager.ts`). `.env.test` leaves `WEB_REVALIDATE_SECRET` empty, so revalidation reports `disabled` while still exposing computed tags for assertions.
|
||||
- `apps/web/vitest.config.ts` maps `#*` to the app root (Vitest ignores `tsconfig` `paths`) and stubs `server-only`, which only resolves inside a Next build.
|
||||
- Not covered: rendering the block and layout components — needs jsdom and `@testing-library/react`, neither a dependency yet.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Web Manager — Public Site (Next.js)
|
||||
|
||||
How the public site renders what [Web Manager](./Readme.md) publishes. Source: `apps/web/web-runtime/`, `apps/web/app/`.
|
||||
|
||||
## Structure
|
||||
|
||||
```txt
|
||||
app/[locale]/[[...path]]/page.tsx localized catch-all
|
||||
app/[locale]/preview/page.tsx token-gated draft rendering, noindex, force-dynamic
|
||||
app/api/revalidate/route.ts secret-guarded tag purge
|
||||
app/sitemap.ts, app/robots.ts generated from the public API
|
||||
web-runtime/api/ HTTP client, render-data resolution, cache tags
|
||||
web-runtime/data/ transport normalization
|
||||
web-runtime/blocks/ block renderers + registry
|
||||
web-runtime/layouts/ page layouts and hero
|
||||
web-runtime/chrome/ header and footer
|
||||
web-runtime/components/ link, rich text and media primitives
|
||||
web-runtime/lib/ metadata, site URL, sitemap, structured data
|
||||
web-runtime/types.ts normalized public contract
|
||||
web-runtime/renderer.tsx header + layout + footer
|
||||
```
|
||||
|
||||
Locales come from `site.config.ts` (`en`, `uk`, `de`, default `en`); with more than one locale
|
||||
next-intl always prefixes the URL.
|
||||
|
||||
Everything degrades rather than fails. An unknown `page_type` falls back to the standard layout; an
|
||||
unknown block type renders a dev-only "renderer missing" banner and nothing at all in production;
|
||||
the normalizer degrades malformed payloads instead of throwing. Publishing a block type the web app
|
||||
does not know yet can therefore never take the site down.
|
||||
|
||||
## Hero
|
||||
|
||||
Hero impact is an intensity choice, not a block type:
|
||||
|
||||
| Impact | Use for | Rendering |
|
||||
| -------- | ------------------------------- | ------------------------------------------------------- |
|
||||
| `none` | pages that start with content | hero omitted |
|
||||
| `low` | legal, utility, index pages | compact padding, smallest title scale |
|
||||
| `middle` | default marketing and content | medium padding, balanced scale |
|
||||
| `high` | campaigns, launches, conversion | near-viewport height, largest scale, radial accent wash |
|
||||
|
||||
Media beside the hero switches it to a two-column grid at every impact. A page with no hero object
|
||||
falls back to a page-type eyebrow plus the title.
|
||||
|
||||
## SEO
|
||||
|
||||
`generateMetadata` delegates to `pageMetadata` (`web-runtime/lib/page-metadata.ts`), a pure function
|
||||
of the resolved render data:
|
||||
|
||||
- **title / description** — the SEO fields, falling back to the page title; an empty description is
|
||||
omitted rather than sent blank.
|
||||
- **canonical** — the page's own locale-prefixed path.
|
||||
- **hreflang** — one alternate per locale in `languageAlternatives`, plus `x-default` pointing at
|
||||
the default locale, or at this page when the default locale has no version. Omitted for a page
|
||||
with no translations.
|
||||
- **Open Graph / Twitter** — the hero image becomes the share card with width, height and alt when
|
||||
supplied; `summary_large_image` with an image, `summary` without; `og:type` is `article` for
|
||||
`blog-item`, `website` otherwise.
|
||||
|
||||
`app/sitemap.ts` walks the paginated sitemap endpoint (capped at 50 requests — the protocol's
|
||||
50,000-URL limit) and hands entries to `buildSitemap`; pages sharing a `groupKey` link to each other
|
||||
through `alternates.languages`, and `lastModified` uses `updatedAt` falling back to `publishedAt`.
|
||||
`app/robots.ts` allows everything except `/*/preview` and advertises the sitemap.
|
||||
`web-runtime/lib/structured-data.tsx` emits `BreadcrumbList` on every page with a trail and
|
||||
`Article` on editorial pages — skipped entirely without a publication date rather than shipping
|
||||
markup a crawler discards.
|
||||
|
||||
**`SITE_URL` must be set.** `metadataBase` comes from `siteUrl()`, and Next resolves every relative
|
||||
metadata URL against it. Without it Next falls back to `http://localhost:3000`: the build succeeds
|
||||
and pages look fine, but canonicals and share cards point at localhost. A production build without
|
||||
it logs an error; a malformed value throws.
|
||||
Reference in New Issue
Block a user