-
-
Notifications
You must be signed in to change notification settings - Fork 1
admin
Documentation Authority: SYSTEM_MODEL.md Section 1 (Tech Stack) and Section 2.3 (Permissions)
The Admin Panel (awcms/) is a React SPA built with Vite. It serves as the central management interface for all tenants.
- SYSTEM_MODEL.md - Primary authority for Admin Panel tech stack (React 19.2.4, Vite 7.3.1, TailwindCSS 4)
- AGENTS.md - Implementation patterns, Context7 references, and multi-tenancy guidelines
- Framework: React 19.2.4
- Build Tool: Vite 7.3.1
- Styling: Tailwind CSS 4, shadcn/ui
- State Management: React Context (Tenant, Permissions, Auth)
- Icons: Lucide React
-
src/components/: Reusable UI components. -
src/contexts/: Global state providers. -
src/hooks/: Custom React hooks. -
src/pages/: Route components. -
src/templates/flowbite-admin/: Main admin layout and shell.
- Client-exposed env vars must use the
VITE_prefix. - Use
import.meta.envin runtime code; useloadEnvinvite.configwhen env values are required at config time.
- Register the module in
resources_registry(scope, db_table, permission_prefix). - Create a
Managercomponent insrc/components/dashboard/. - Add a route in
src/components/MainRouter.jsx(use/<module>/*if the module needs sub-slugs for tabs or trash views). - Insert a sidebar item in
admin_menus(seed viaawcms/src/scripts/seed-sidebar.js). - Ensure the permission exists in
permissionsand is mapped viarole_permissions.
Use the usePermissions hook to guard UI elements:
const { hasPermission } = usePermissions();
if (hasPermission('tenant.blog.create')) {
<Button>Create Article</Button>
}Plugins can inject dashboard widgets via the dashboard_widgets filter. Use plugin registry keys for components (for example mailketing:MailketingCreditsWidget).
addFilter('dashboard_widgets', 'mailketing_stats', (widgets) => [
...widgets,
{
id: 'mailketing_credits',
component: 'mailketing:MailketingCreditsWidget',
position: 'sidebar',
priority: 50,
frame: false
}
]);The useTenant hook provides the currently selected tenant context. All API calls should include tenant_id unless they are super-admin global operations.
Local development uses VITE_DEV_TENANT_SLUG (default primary) on localhost. If tenant resolution fails, run node awcms/src/scripts/seed-primary-tenant.js.
const { currentTenant } = useTenant();
// Use currentTenant.id in mutationsCreate a tenant-aware form that inserts draft content while enforcing permissions and author ownership.
| Field | Source | Required | Notes |
|---|---|---|---|
tenantId |
useTenant() |
Yes | Scope for all inserts |
| Permission | usePermissions() |
Yes | tenant.blog.create |
author_id |
auth.getUser() |
Yes | Do not trust caller input |
slug |
Derived from title | Yes | Enforce per-tenant uniqueness |
- Block submit if tenant context is missing.
- Block submit if permission is missing.
- Resolve current user and build payload with
status: 'draft'. - Insert via
customSupabaseClientand handle duplicate slug errors. - Show toast on success/failure and reset form state.
import { useState } from "react";
import { supabase } from "@/lib/customSupabaseClient";
import { useTenant } from "@/contexts/TenantContext";
import { usePermissions } from "@/contexts/PermissionContext";
import { useToast } from "@/components/ui/use-toast";
const toSlug = (value) =>
value
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
export default function CreateBlogPostForm() {
const { tenantId } = useTenant();
const { hasPermission } = usePermissions();
const { toast } = useToast();
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (event) => {
event.preventDefault();
if (!tenantId) {
toast({ variant: "destructive", title: "Missing tenant context" });
return;
}
if (!hasPermission("tenant.blog.create")) {
toast({ variant: "destructive", title: "Permission denied" });
return;
}
setLoading(true);
const { data: authData } = await supabase.auth.getUser();
const user = authData?.user;
if (!user) {
setLoading(false);
toast({ variant: "destructive", title: "Session expired" });
return;
}
const { error } = await supabase.from("blogs").insert({
tenant_id: tenantId,
author_id: user.id,
title,
content,
slug: toSlug(title),
status: "draft",
});
setLoading(false);
if (error) {
toast({
variant: "destructive",
title: "Create failed",
description: error.code === "23505" ? "Slug already exists." : error.message,
});
return;
}
toast({ title: "Saved", description: "Draft created." });
setTitle("");
setContent("");
};
return (
<form onSubmit={handleSubmit}>
{/* inputs and submit button */}
</form>
);
}- Insert is blocked without tenant context.
- User without
tenant.blog.createcannot submit. -
author_idmatches authenticated user. - Duplicate slugs return a friendly error.
- Hardcoded tenant IDs: always use
useTenant(). - Publishing on create: keep
status = 'draft'and require publish permission. - Silent errors: use toast feedback for all error paths.