Compare commits
4 Commits
improvemen
...
v1.0.11
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d2c279f4b | |||
| fb59aed2f8 | |||
| 4add3ab859 | |||
| ea6976351a |
26
.cursor/rules/adminjs.mdc
Normal file
26
.cursor/rules/adminjs.mdc
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
description: AdminJS panel must stay in sync with Prisma schema changes
|
||||||
|
globs: backend/src/admin/**,backend/prisma/schema.prisma,backend/prisma/migrations/**
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# AdminJS ↔ Prisma sync (required)
|
||||||
|
|
||||||
|
Ops panel at `/admin` (`backend/src/admin/`). Resources are a **manual allowlist** — Prisma does **not** auto-update AdminJS.
|
||||||
|
|
||||||
|
## When you change `schema.prisma`
|
||||||
|
|
||||||
|
**Before finishing the task**, update AdminJS:
|
||||||
|
|
||||||
|
1. Open [`backend/src/admin/resources.ts`](backend/src/admin/resources.ts) (`buildAdminResources`).
|
||||||
|
2. **New model** ops may need to inspect/fix → add `resource(...)` + navigation group + hide secrets.
|
||||||
|
3. **Renamed / removed model** → update or remove the matching resource (broken `getModelByName` breaks `/admin` boot).
|
||||||
|
4. **New secret fields** (hashes, tokens, share tokens) → hide via `isVisible: false` (list/filter/show/edit).
|
||||||
|
5. **Catalog-like reference data** → list/show/edit only; disable `new` / `delete` / `bulkDelete`.
|
||||||
|
6. Skip pure join/cursor tables unless ops need them (`LabCaseUserReadState`, `LabCaseUserTabReadState`, working-hours, `LabCaseAttachment`).
|
||||||
|
|
||||||
|
Auth: `ADMINJS_EMAIL` / `ADMINJS_PASSWORD` — production login disabled if password missing or still `admin123`.
|
||||||
|
|
||||||
|
## Secrets to hide
|
||||||
|
|
||||||
|
`passwordHash`, session `token`/`refreshToken`, invite/OTP `tokenHash`/`codeHash`, `LabCase.accessToken`.
|
||||||
@@ -35,6 +35,7 @@ throw new AppException(ErrorCode.PERMISSION_DENIED, HttpStatus.FORBIDDEN);
|
|||||||
- Schema: `backend/prisma/schema.prisma`
|
- Schema: `backend/prisma/schema.prisma`
|
||||||
- Always add a migration for schema changes (`npm run prisma:migrate` in backend).
|
- Always add a migration for schema changes (`npm run prisma:migrate` in backend).
|
||||||
- Seed permissions stay in sync with `ALL_TAB_PERMISSIONS` in `common/permissions.ts`.
|
- Seed permissions stay in sync with `ALL_TAB_PERMISSIONS` in `common/permissions.ts`.
|
||||||
|
- **Schema change ⇒ AdminJS:** update `backend/src/admin/resources.ts` in the same change (add/rename/remove resources, hide new secrets). See `.cursor/rules/adminjs.mdc`.
|
||||||
|
|
||||||
## API responses
|
## API responses
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Monorepo: `backend/` (NestJS + Prisma), `frontend/` (Next.js + next-intl), `infr
|
|||||||
- **Never commit or push** unless the user explicitly asks.
|
- **Never commit or push** unless the user explicitly asks.
|
||||||
- Prefer minimal diffs; reuse existing components and API patterns.
|
- Prefer minimal diffs; reuse existing components and API patterns.
|
||||||
- After cross-cutting changes: `backend` → `npm run build`; `frontend` → `npx tsc --noEmit`.
|
- After cross-cutting changes: `backend` → `npm run build`; `frontend` → `npx tsc --noEmit`.
|
||||||
|
- Prisma `schema.prisma` changes ⇒ update AdminJS allowlist (`backend/src/admin/resources.ts`) — `.cursor/rules/adminjs.mdc`.
|
||||||
|
|
||||||
## i18n
|
## i18n
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Follow this checklist. Adapt steps if the feature is read-only or org-type-speci
|
|||||||
|
|
||||||
```
|
```
|
||||||
- [ ] 1. Permissions & org type
|
- [ ] 1. Permissions & org type
|
||||||
- [ ] 2. Backend module
|
- [ ] 2. Backend module (+ Prisma / AdminJS if new models)
|
||||||
- [ ] 3. Frontend UI + thin page
|
- [ ] 3. Frontend UI + thin page
|
||||||
- [ ] 4. i18n (en, fa, nl)
|
- [ ] 4. i18n (en, fa, nl)
|
||||||
- [ ] 5. Verify build / tsc
|
- [ ] 5. Verify build / tsc
|
||||||
@@ -40,6 +40,7 @@ backend/src/modules/{feature}/
|
|||||||
- Service-level permission checks with `hasEffectivePermission`.
|
- Service-level permission checks with `hasEffectivePermission`.
|
||||||
- DTOs use `ErrorCode` validation messages.
|
- DTOs use `ErrorCode` validation messages.
|
||||||
- Register in `app.module.ts`.
|
- Register in `app.module.ts`.
|
||||||
|
- If you add/change Prisma models: update AdminJS allowlist in `backend/src/admin/resources.ts` (same PR). See `.cursor/rules/adminjs.mdc`.
|
||||||
|
|
||||||
## 3. Frontend
|
## 3. Frontend
|
||||||
|
|
||||||
|
|||||||
@@ -86,11 +86,14 @@ frontend/src/
|
|||||||
backend/src/
|
backend/src/
|
||||||
modules/{feature}/ → controller, service, dto, module
|
modules/{feature}/ → controller, service, dto, module
|
||||||
common/ → guards, permissions, errors, utils
|
common/ → guards, permissions, errors, utils
|
||||||
|
admin/ → AdminJS `/admin` panel (curated Prisma resources)
|
||||||
prisma/ → schema, migrations, seed
|
prisma/ → schema, migrations, seed
|
||||||
```
|
```
|
||||||
|
|
||||||
Errors: `AppException` + `ErrorCode` → frontend `getUserFacingError()`. Unexpected 500s: GlitchTip (`SENTRY_DSN`). Never throw raw strings for user-facing failures.
|
Errors: `AppException` + `ErrorCode` → frontend `getUserFacingError()`. Unexpected 500s: GlitchTip (`SENTRY_DSN`). Never throw raw strings for user-facing failures.
|
||||||
|
|
||||||
|
**AdminJS:** Manual resource allowlist in `backend/src/admin/resources.ts`. **Whenever `schema.prisma` changes**, update AdminJS resources in the same task (new/renamed/removed models, hide secrets). Rule: `.cursor/rules/adminjs.mdc`.
|
||||||
|
|
||||||
## Git & commits
|
## Git & commits
|
||||||
|
|
||||||
- **Do not commit or push** unless the user explicitly asks.
|
- **Do not commit or push** unless the user explicitly asks.
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ Clinics may only dispatch to labs they are linked to: `OrganizationLink` (A↔B,
|
|||||||
### Layout conventions worth knowing before you create a file
|
### Layout conventions worth knowing before you create a file
|
||||||
|
|
||||||
- **Prisma lives outside `src/`**: `backend/prisma/` holds `schema.prisma`, migrations, seeds *and* `prisma.module.ts` / `prisma.service.ts` — hence imports like `../../../prisma/prisma.service`. Register new Nest modules in `app.module.ts`.
|
- **Prisma lives outside `src/`**: `backend/prisma/` holds `schema.prisma`, migrations, seeds *and* `prisma.module.ts` / `prisma.service.ts` — hence imports like `../../../prisma/prisma.service`. Register new Nest modules in `app.module.ts`.
|
||||||
|
- **AdminJS (`/admin`)** resources are a manual allowlist in `backend/src/admin/resources.ts` — update them whenever `schema.prisma` changes (see `.cursor/rules/adminjs.mdc`).
|
||||||
- **Frontend layering** (`.cursor/rules/frontend-components.mdc`): `app/**/page.tsx` is a thin wrapper only → route logic in `components/ui/{feature}/{Feature}Page.tsx` → JSX in `components/ui/**` → pure helpers in `components/{feature}/` or `components/shared/`. No JSX outside `ui/`, no pure helpers inside it.
|
- **Frontend layering** (`.cursor/rules/frontend-components.mdc`): `app/**/page.tsx` is a thin wrapper only → route logic in `components/ui/{feature}/{Feature}Page.tsx` → JSX in `components/ui/**` → pure helpers in `components/{feature}/` or `components/shared/`. No JSX outside `ui/`, no pure helpers inside it.
|
||||||
- **i18n is mandatory, not a follow-up**: every user-visible string goes into `en.json`, `fa.json`, **and** `nl.json`. `fa` is RTL, so use logical `text-start`/`text-end`, never `text-left`/`text-right`. Dates/times/numbers go through `lib/i18n/format.ts`; form dates use `AppDateInput`, never a native date input.
|
- **i18n is mandatory, not a follow-up**: every user-visible string goes into `en.json`, `fa.json`, **and** `nl.json`. `fa` is RTL, so use logical `text-start`/`text-end`, never `text-left`/`text-right`. Dates/times/numbers go through `lib/i18n/format.ts`; form dates use `AppDateInput`, never a native date input.
|
||||||
- Treatment attachments are written to disk at `backend/uploads/treatments` relative to `process.cwd()`.
|
- Treatment attachments are written to disk at `backend/uploads/treatments` relative to `process.cwd()`.
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
// backend/src/admin/admin.module.ts
|
|
||||||
import { DynamicModule, Module } from '@nestjs/common';
|
import { DynamicModule, Module } from '@nestjs/common';
|
||||||
import { PrismaService } from '../../prisma/prisma.service';
|
import { PrismaService } from '../../prisma/prisma.service';
|
||||||
import { componentLoader, Components } from './components';
|
import { componentLoader, Components } from './components';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { Database, Resource, getModelByName } from '@adminjs/prisma'; // 👈 Add getModelByName
|
import { Database, Resource } from '@adminjs/prisma';
|
||||||
import AdminJS from 'adminjs';
|
import AdminJS from 'adminjs';
|
||||||
|
import { buildAdminResources } from './resources';
|
||||||
|
|
||||||
// Register the adapter
|
|
||||||
AdminJS.registerAdapter({ Database, Resource });
|
AdminJS.registerAdapter({ Database, Resource });
|
||||||
|
|
||||||
|
const LOCAL_DEFAULT_ADMIN_PASSWORD = 'admin123';
|
||||||
|
const LOCAL_DEFAULT_ADMIN_EMAIL = 'admin@dyolink.com';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
})
|
})
|
||||||
@@ -17,10 +19,24 @@ export class AdminModule {
|
|||||||
const { AdminModule: AdminJSModule } = await import('@adminjs/nestjs');
|
const { AdminModule: AdminJSModule } = await import('@adminjs/nestjs');
|
||||||
|
|
||||||
const authenticate = async (email: string, password: string) => {
|
const authenticate = async (email: string, password: string) => {
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
const adminEmail =
|
const adminEmail =
|
||||||
process.env.ADMINJS_EMAIL?.trim() || 'admin@dyolink.com';
|
process.env.ADMINJS_EMAIL?.trim() || LOCAL_DEFAULT_ADMIN_EMAIL;
|
||||||
const adminPassword = process.env.ADMINJS_PASSWORD || 'admin123';
|
const adminPassword = process.env.ADMINJS_PASSWORD;
|
||||||
if (email === adminEmail && password === adminPassword) {
|
|
||||||
|
if (isProduction) {
|
||||||
|
if (
|
||||||
|
!adminPassword ||
|
||||||
|
adminPassword === LOCAL_DEFAULT_ADMIN_PASSWORD
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectivePassword =
|
||||||
|
adminPassword || LOCAL_DEFAULT_ADMIN_PASSWORD;
|
||||||
|
|
||||||
|
if (email === adminEmail && password === effectivePassword) {
|
||||||
return { email, role: 'admin' };
|
return { email, role: 'admin' };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -38,68 +54,54 @@ export class AdminModule {
|
|||||||
config.get<string>('jwt.secret') ||
|
config.get<string>('jwt.secret') ||
|
||||||
config.get('JWT_SECRET') ||
|
config.get('JWT_SECRET') ||
|
||||||
'secret-key-change-this';
|
'secret-key-change-this';
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
const adminPassword = process.env.ADMINJS_PASSWORD;
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV === 'production' &&
|
!adminPassword ||
|
||||||
!process.env.ADMINJS_PASSWORD
|
adminPassword === LOCAL_DEFAULT_ADMIN_PASSWORD
|
||||||
) {
|
) {
|
||||||
console.warn(
|
console.error(
|
||||||
'⚠️ ADMINJS_PASSWORD is unset; AdminJS is using the local default. Set it in backend.env.',
|
'❌ AdminJS: ADMINJS_PASSWORD is missing or still the local default. Login is disabled until you set a strong password in backend.env.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
adminJsOptions: {
|
adminJsOptions: {
|
||||||
rootPath: '/admin',
|
rootPath: '/admin',
|
||||||
resources: [
|
resources: buildAdminResources(prisma),
|
||||||
// ✅ Use getModelByName helper
|
|
||||||
{
|
|
||||||
resource: {
|
|
||||||
model: getModelByName('User'),
|
|
||||||
client: prisma,
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
properties: {
|
|
||||||
passwordHash: { isVisible: false },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resource: {
|
|
||||||
model: getModelByName('Organization'),
|
|
||||||
client: prisma,
|
|
||||||
},
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resource: {
|
|
||||||
model: getModelByName('OrganizationType'),
|
|
||||||
client: prisma,
|
|
||||||
},
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resource: {
|
|
||||||
model: getModelByName('Plan'),
|
|
||||||
client: prisma,
|
|
||||||
},
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resource: {
|
|
||||||
model: getModelByName('Membership'),
|
|
||||||
client: prisma,
|
|
||||||
},
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
resource: {
|
|
||||||
model: getModelByName('Session'),
|
|
||||||
client: prisma,
|
|
||||||
},
|
|
||||||
options: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
componentLoader,
|
componentLoader,
|
||||||
dashboard: { component: Components.Dashboard },
|
dashboard: {
|
||||||
|
component: Components.Dashboard,
|
||||||
|
handler: async () => {
|
||||||
|
const [clinicType, labType] = await Promise.all([
|
||||||
|
prisma.organizationType.findUnique({
|
||||||
|
where: { name: 'CLINIC' },
|
||||||
|
}),
|
||||||
|
prisma.organizationType.findUnique({
|
||||||
|
where: { name: 'LAB' },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [clinics, labs, users, labCases] = await Promise.all([
|
||||||
|
clinicType
|
||||||
|
? prisma.organization.count({
|
||||||
|
where: { typeId: clinicType.id },
|
||||||
|
})
|
||||||
|
: Promise.resolve(0),
|
||||||
|
labType
|
||||||
|
? prisma.organization.count({
|
||||||
|
where: { typeId: labType.id },
|
||||||
|
})
|
||||||
|
: Promise.resolve(0),
|
||||||
|
prisma.user.count(),
|
||||||
|
prisma.labCase.count(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { clinics, labs, users, labCases };
|
||||||
|
},
|
||||||
|
},
|
||||||
branding: {
|
branding: {
|
||||||
companyName: 'DyoLink Admin',
|
companyName: 'DyoLink Admin',
|
||||||
logo: false,
|
logo: false,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ const componentLoader = new ComponentLoader();
|
|||||||
|
|
||||||
const Components = {
|
const Components = {
|
||||||
Dashboard: componentLoader.add('Dashboard', './dashboard'),
|
Dashboard: componentLoader.add('Dashboard', './dashboard'),
|
||||||
// You can add more components here as needed
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { componentLoader, Components };
|
export { componentLoader, Components };
|
||||||
@@ -1,29 +1,78 @@
|
|||||||
// backend/src/admin/dashboard-simple.tsx
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import React from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Box, H2, Text, Badge } from '@adminjs/design-system';
|
import { Box, H2, Text } from '@adminjs/design-system';
|
||||||
|
import { ApiClient } from 'adminjs';
|
||||||
|
|
||||||
|
type DashboardStats = {
|
||||||
|
clinics: number;
|
||||||
|
labs: number;
|
||||||
|
users: number;
|
||||||
|
labCases: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatCard = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
loading,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: number;
|
||||||
|
loading: boolean;
|
||||||
|
}) => (
|
||||||
|
<Box p="lg" bg="primary20" style={{ flex: 1, minWidth: '140px' }}>
|
||||||
|
<Text>{label}</Text>
|
||||||
|
<Box mt="default" style={{ fontSize: '2rem', fontWeight: 'bold' }}>
|
||||||
|
{loading ? '…' : (value ?? '—')}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const api = new ApiClient();
|
||||||
|
api
|
||||||
|
.getDashboard()
|
||||||
|
.then((response) => {
|
||||||
|
setStats(response.data as DashboardStats);
|
||||||
|
setError(null);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setError('Could not load dashboard stats.');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box variant="grey">
|
<Box variant="grey">
|
||||||
<Box variant="white" p="xl">
|
<Box variant="white" p="xl">
|
||||||
<H2>Welcome to DyoLink Admin Panel</H2>
|
<H2>DyoLink Admin</H2>
|
||||||
<Text>Manage your dental clinics, labs, users, and subscriptions.</Text>
|
<Text>Manage clinics, labs, users, and production data.</Text>
|
||||||
|
|
||||||
<Box mt="xl" style={{ display: 'flex', gap: '20px' }}>
|
{error ? (
|
||||||
<Box p="lg" bg="primary20" style={{ flex: 1 }}>
|
<Box mt="xl">
|
||||||
<div style={{ fontSize: '1.5rem' }}>🏥 Clinics</div>
|
<Text>{error}</Text>
|
||||||
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>12</div>
|
|
||||||
</Box>
|
|
||||||
<Box p="lg" bg="secondary20" style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: '1.5rem' }}>🔬 Labs</div>
|
|
||||||
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>8</div>
|
|
||||||
</Box>
|
|
||||||
<Box p="lg" bg="info20" style={{ flex: 1 }}>
|
|
||||||
<div style={{ fontSize: '1.5rem' }}>👥 Users</div>
|
|
||||||
<div style={{ fontSize: '2rem', fontWeight: 'bold' }}>45</div>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
mt="xl"
|
||||||
|
style={{ display: 'flex', gap: '20px', flexWrap: 'wrap' }}
|
||||||
|
>
|
||||||
|
<StatCard label="Clinics" value={stats?.clinics} loading={loading} />
|
||||||
|
<StatCard label="Labs" value={stats?.labs} loading={loading} />
|
||||||
|
<StatCard label="Users" value={stats?.users} loading={loading} />
|
||||||
|
<StatCard
|
||||||
|
label="Lab cases"
|
||||||
|
value={stats?.labCases}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
169
backend/src/admin/resources.ts
Normal file
169
backend/src/admin/resources.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { getModelByName } from '@adminjs/prisma';
|
||||||
|
import type { PrismaService } from '../../prisma/prisma.service';
|
||||||
|
|
||||||
|
type ResourceOptions = {
|
||||||
|
navigation?: { name: string; icon?: string };
|
||||||
|
properties?: Record<string, { isVisible?: boolean | { list?: boolean; filter?: boolean; show?: boolean; edit?: boolean } }>;
|
||||||
|
actions?: Record<
|
||||||
|
string,
|
||||||
|
{ isAccessible?: boolean }
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AdminResource = {
|
||||||
|
resource: { model: ReturnType<typeof getModelByName>; client: PrismaService };
|
||||||
|
options: ResourceOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hide = (...propertyNames: string[]): ResourceOptions['properties'] =>
|
||||||
|
Object.fromEntries(
|
||||||
|
propertyNames.map((name) => [
|
||||||
|
name,
|
||||||
|
{ isVisible: { list: false, filter: false, show: false, edit: false } },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const catalogActions: ResourceOptions['actions'] = {
|
||||||
|
new: { isAccessible: false },
|
||||||
|
delete: { isAccessible: false },
|
||||||
|
bulkDelete: { isAccessible: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
function resource(
|
||||||
|
client: PrismaService,
|
||||||
|
modelName: string,
|
||||||
|
options: ResourceOptions = {},
|
||||||
|
): AdminResource {
|
||||||
|
return {
|
||||||
|
resource: {
|
||||||
|
model: getModelByName(modelName),
|
||||||
|
client,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Curated AdminJS allowlist — keep in sync when adding ops-relevant Prisma models. */
|
||||||
|
export function buildAdminResources(prisma: PrismaService): AdminResource[] {
|
||||||
|
return [
|
||||||
|
// Identity
|
||||||
|
resource(prisma, 'User', {
|
||||||
|
navigation: { name: 'Identity', icon: 'User' },
|
||||||
|
properties: hide('passwordHash'),
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Session', {
|
||||||
|
navigation: { name: 'Identity', icon: 'User' },
|
||||||
|
properties: hide('token', 'refreshToken'),
|
||||||
|
}),
|
||||||
|
resource(prisma, 'PhoneVerificationCode', {
|
||||||
|
navigation: { name: 'Identity', icon: 'User' },
|
||||||
|
properties: hide('codeHash'),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Orgs & access
|
||||||
|
resource(prisma, 'Organization', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'OrganizationType', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Plan', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Membership', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Permission', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'MembershipPermission', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Feature', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'StaffInvitation', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
properties: hide('tokenHash'),
|
||||||
|
}),
|
||||||
|
resource(prisma, 'OrganizationLink', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'OrganizationInvitation', {
|
||||||
|
navigation: { name: 'Orgs & access', icon: 'Settings' },
|
||||||
|
properties: hide('tokenHash'),
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Clinic
|
||||||
|
resource(prisma, 'Patient', {
|
||||||
|
navigation: { name: 'Clinic', icon: 'Healthcare' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Appointment', {
|
||||||
|
navigation: { name: 'Clinic', icon: 'Healthcare' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'Treatment', {
|
||||||
|
navigation: { name: 'Clinic', icon: 'Healthcare' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'TreatmentDetail', {
|
||||||
|
navigation: { name: 'Clinic', icon: 'Healthcare' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'TreatmentDetailAttachment', {
|
||||||
|
navigation: { name: 'Clinic', icon: 'Healthcare' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Lab
|
||||||
|
resource(prisma, 'LabCase', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
properties: hide('accessToken'),
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseLine', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseDetail', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseSend', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseToothProsthesis', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseTask', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseTaskStatusEvent', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseComment', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabCaseActivity', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
resource(prisma, 'UserNotification', {
|
||||||
|
navigation: { name: 'Lab', icon: 'Archive' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Catalog — edit OK; create/delete via seed/migrations
|
||||||
|
resource(prisma, 'TreatmentType', {
|
||||||
|
navigation: { name: 'Catalog', icon: 'Catalog' },
|
||||||
|
actions: catalogActions,
|
||||||
|
}),
|
||||||
|
resource(prisma, 'ProsthesisType', {
|
||||||
|
navigation: { name: 'Catalog', icon: 'Catalog' },
|
||||||
|
actions: catalogActions,
|
||||||
|
}),
|
||||||
|
resource(prisma, 'LabWorkflowStep', {
|
||||||
|
navigation: { name: 'Catalog', icon: 'Catalog' },
|
||||||
|
actions: catalogActions,
|
||||||
|
}),
|
||||||
|
resource(prisma, 'ProsthesisTypeStep', {
|
||||||
|
navigation: { name: 'Catalog', icon: 'Catalog' },
|
||||||
|
actions: catalogActions,
|
||||||
|
}),
|
||||||
|
resource(prisma, 'CatalogTranslation', {
|
||||||
|
navigation: { name: 'Catalog', icon: 'Catalog' },
|
||||||
|
actions: catalogActions,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user