加拉德明语Name
加拉德明是一个工具 由 加来德林 让你在几分钟内设置一个漂亮的后台 有用吗 与 MySQL 数据库连接.
快开始
npm install @galadrim/galadmin
融入 NestJS 应用程序
把这个密码放进你的 src/main.ts 文件 :
import galadmin from '@galadrim/galadmin';
import { Config } from '@galadrim/galadmin/dist/types/config';
const adminConfig: Config = { ... }
app.getHttpAdapter().use('/admin', galadmin(adminConfig));
如果您正在使用 Prisma ORM, 请在您的 schema.prisma。 。 。 .
加拉德明会在你的数据库里创建一个表格,而Prisma将无法
如果我们不指定,则正确执行迁移.
使用 PostgreSQL
model galadmin_sessions {
sid String @id @db.VarChar(128)
sess Json
expire DateTime
}
带着 MySQL
model galadmin_sessions {
session_id String @id @db.VarChar(128)
expires Int @db.UnsignedInt
data String? @db.MediumText
}
在 Express 应用程序中的整合
const galadmin = require("@galadrim/galadmin").default;
app.use("/admin", galadmin(adminConfig));
行政主管是什么?
管理员配置 是包含全部配置信息的对象 关于你的后台。 其属性列于下.
最小示例
const adminConfig = {
name: "Mon projet",
// Secrets come from the environment, never from the code:
// generate the session secret with `openssl rand -hex 32`.
sessionSecret: process.env.GALADMIN_SESSION_SECRET,
auth: {
username: "admin",
password: process.env.GALADMIN_ADMIN_PASSWORD,
},
postgresql: {
host: "abc.example.com",
user: "monprojet",
password: process.env.DB_PASSWORD,
database: "monprojet",
},
// mysql: {
// host: "abc.example.com",
// user: "root",
// password: "password",
// database: "monprojet",
// },
views: [
{
type: "table",
label: "Fruits",
tableName: "fruits",
columns: [
{
label: "Nom",
name: "fruitName",
},
{
label: "Acidité",
name: "acidity",
},
],
},
],
};
配置
const adminConfig = {
/** Nom du projet (OBLIGATOIRE) */
name: "Mon projet",
/**
* Chaîne de caractères aléatoires qui permet de chiffrer les cookies de session (OBLIGATOIRE).
* À générer (`openssl rand -hex 32`) et à lire depuis l'environnement, jamais en dur dans le code.
*/
sessionSecret: process.env.GALADMIN_SESSION_SECRET,
/** Identifiants permettant d'accéder au back-office (OBLIGATOIRE) */
auth: {
/** Nom d'utilisateur */
username: "admin",
/** Mot de passe, long et unique, lu depuis l'environnement */
password: process.env.GALADMIN_ADMIN_PASSWORD,
},
/** Identifiants de connexion à la base de données (OBLIGATOIRE) */
postgresql: {
host: "abc.example.com",
user: "monprojet",
password: process.env.DB_PASSWORD,
database: "monprojet",
},
// mysql: {
// host: "abc.example.com",
// user: "root",
// password: "password",
// database: "monprojet",
// },
/** Clé API Google Maps pour les champs adresses et l'affichage de cartes */
googleMapsKey: "______",
/**
* Origine autorisée à appeler l'API avec les cookies de session depuis un autre domaine
* (en-têtes CORS). Aucune par défaut ; "http://localhost:3000" pour le serveur de dev du front.
*/
corsOrigin: undefined,
/** Taille maximale, en octets, des fichiers d'un upload ou d'un import (illimitée par défaut) */
uploadMaxFileSize: 50 * 1024 * 1024,
/** Désactiver la sélection multiple dans les vues de type table */
hideMultiselect: true,
/** Cache l'inscription Made with love by Galadrim */
hideBranding: true,
views: [
{
/** Vue de type "table" */
type: "table",
/** Nom affiché de la vue */
label: string,
/** Icône FontAwesome de la vue dans le menu. user affiche l'icône fa-user. */
icon: "user",
/** Nom de la table MySQL */
tableName: "users",
/** Nombre de résultats par page */
limit: 100,
/**
* Ces champs permettent de personnaliser la requête MySQL qui gère l'affichage de la table.
* Ils sont tous optionnels.
*
* SELECT {selectExpression} FROM {tableName} {join} WHERE {filters} GROUP BY {groupBy} ORDER BY {orderBy} {desc ? "DESC" : ""}
*/
selectExpression: "*, MAX(age) AS maxAge",
join: "JOIN activities ON users.activity_id = activities.id",
groupBy: "activity_id",
orderBy: "maxAge",
isDesc: true,
filters: "age < 50",
/** Lorsque notClickable est à true, les résultats ne sont pas cliquables. (default: false) */
notClickable: true,
/** Rendre possible de créer des enregistrements (default: false) */
canCreate: true,
/** Groupe nominal "un nouvel élément" */
aNewSingular: "un nouvel utilisateur",
/** Rendre possible de supprimer des enregistrements (default: true) */
canDelete: true,
/** Rendre possible de modifier des enregistrements (default: true) */
canEdit: true,
/** URL appelée en POST lors de la création d'un nouvel élément, avec l'ID de l'élément créé */
createCallback: "https://myapi.com/createCallback",
editCallback: "https://myapi.com/editCallback",
deleteCallback: "https://myapi.com/deleteCallback",
columns: [
{
/** Nom affiché de la colonne (OBLIGATOIRE) */
label: "Poids",
/** Nom de la colonne dans la table MySQL */
name: "weight",
/** Formatter la valeur avant de l'afficher */
formatValue: (user) => `${user.weight} kg`,
/** Masquer la colonne dans la liste des résultats et ne l'afficher que dans le détail (default: false) */
hideInTable: true,
},
],
subviews: [],
recordViewPanels: [],
},
],
};
双要素认证( TOTP)
报表 authRoles 可以使用认证器应用程序(Google认证器, Authy...). 词根 auth 账户和账户 trustedAuth 永远不会受到影响。 无 twoFactor,没有什么变化.
authRoles: [
{
role: "admin",
table: "Admin",
usernameField: "username",
passwordField: "password",
idField: "id",
twoFactor: {
/** Colonne texte nullable qui stocke le secret TOTP ; null = 2FA désactivée pour ce compte (OBLIGATOIRE) */
secretField: "twoFactorSecret",
/** Oblige chaque compte à configurer la 2FA à sa prochaine connexion, sans pouvoir la désactiver (default: false) */
enforce: false,
/** Nom affiché dans l'application d'authentification (default: config.name) */
issuer: "Mon projet",
/** Chiffre les secrets en base (AES-256-GCM). Valeur aléatoire longue, stockée hors de la base. La changer rend les secrets existants illisibles */
encryptionKey: process.env.GALADMIN_2FA_KEY,
/** Colonnes nullables entier + date/heure : verrouille le compte 15 minutes après 5 codes erronés */
failedAttemptsField: "twoFactorFailedAttempts",
lockedUntilField: "twoFactorLockedUntil",
/** Colonne bigint nullable : refuse un code déjà utilisé */
lastUsedStepField: "twoFactorLastUsedStep",
},
},
],
当2FA是可选时,用户从侧边栏菜单"双认证"中打开或关闭. 要重置用户的 2FA( 丢失的电话), 设置 secretField 恢复为无效。 从不显示 secretField 在风景。 这个角色的 table 必须是可写入的(不是 SQL 视图).
没有锁出栏,只将一个登录在5个错误代码后发回密码步骤,所以重复的尝试不会被每个账户节制.
密码更改和过期
报表 authRoles 可以更改自己的密码,密码也可以过期. 词根 auth 账户和账户 trustedAuth 永远不会受到影响。 无 passwordPolicy,没有什么变化。 不管有没有用 twoFactor。 。 。 .
authRoles: [
{
role: "admin",
table: "Admin",
usernameField: "username",
passwordField: "password",
idField: "id",
useArgon2: true,
passwordPolicy: {
/** Ajoute "Changer mon mot de passe" au menu (default: false, toujours actif avec expiry) */
allowChangeOwn: true,
/** Vérifie un nouveau mot de passe : renvoie le message affiché à l'utilisateur, ou null s'il est accepté (sync ou async) */
validate: (password) => (password.length >= 12 ? null : "Le mot de passe doit contenir au moins 12 caractères"),
expiry: {
/** Colonne date/heure nullable : date du dernier changement ; null = changement demandé à la prochaine connexion (OBLIGATOIRE) */
changedAtField: "passwordChangedAt",
/** Durée de validité d'un mot de passe, en jours (OBLIGATOIRE) */
maxAgeDays: 365,
/** Un bandeau invite à changer de mot de passe pendant les N derniers jours (default: 30) */
warnDays: 30,
},
},
},
],
更改表请求当前密码, 拒绝当前密码作为新密码, 运行 validate,然后在 useArgon2 已经设定, bcrypt 否则。 当前密码必须存储散列: 密码被存储的账户不能从galadmin中更改 .
与 expiry,登录密码比 maxAgeDays (或 changedAtField 无效)在打开后台前,在2FA代码存在后,请求新的密码。 允许在一张空的桌子上 changedAtField 使每个账户在下次登录时选择新的密码;首先用当前日期填入列来避免.
validate 财务报告和审定财务报表 changedAtField 仅适用于从galadmin更改的密码。 在视图中输入的密码是写成的 : 散列并在您一边验证, 然后设定 changedAtField (例如,在下次登录时强制更改无效).
警卫
- 密码
authRoles账户 须以散列( bcrypt, 或 argon2) 存储useArgon2) (中文(简体) ). 保存所需密码的遗留表格allowPlaintextPasswords: true;即使如此,看起来像散列的存储值也从未像现在这样比较,空密码也从未登录。 尽快取消密码. - 登录尝试 限制:在一个地址的用户名失败5次后,其登录被拒绝15分钟(包括root账户)。 计数器在过程内存中直播.
filtersFromUser财务报告和审定财务报表filters绑定每个读取或修改一个记录的路由(/record, (中文(简体) ),/updateRecord, (中文(简体) ),/deleteRecord(s), (中文(简体) ),/customAction,面板行):列表中不显示答案的行404. 以customQuery不受限制,因为其列表既不适用过滤器./api/config从不发送数据库证书,auth, (中文(简体) ),authRoles, (中文(简体) ),trustedAuth,或者s3Config编号 :gcpConfig编号 :sftpConfig编号 :publicPath页:1.- 上传 保持他们的
public-readS3上的ACL, 但 HTML, SVG, XML 和 JavaScript 文件被存储Content-Disposition: attachment在S3和GCP上,这样打开他们的URL下载. 本地或超过 SFTP 写入的文件由您自己的服务器服务: 不服务后台域的内置这些类型 . - 会议饼干是
HttpOnly, (中文(简体) ),SameSite=Lax,以及Secure当请求通过 HTTPS (设置)app.set("trust proxy", 1)在 TLS 终止代理后面). 每个登录获得新的会话编号;POST /api/logout关闭日志 。 答复包括:X-Frame-Options: SAMEORIGIN财务报告和审定财务报表X-Content-Type-Options: nosniff。 。 。 .
从0.3.6升级: GET /api/logout 成为 POST, (中文(简体) ), /api/adjacentRecordId (未被正面使用)不见了, CORS 信头需要 corsOrigin,并按需要存储密码 allowPlaintextPasswords。 。 。 .
边栏文件夹
视图可在侧边栏中组合为 type: "folder" 视图。 一个文件夹是
从来没有页面本身: 它只分组其他视图, 所以组的 URL
视图与根部所申报的完全相同.
views: [
{
type: "folder",
/** Nom affiché du dossier */
label: "Catalogue",
/** Ouvrir le dossier par défaut (default: false).
Un dossier contenant la vue active s'ouvre automatiquement. */
defaultOpen: true,
/** Vues regroupées dans le dossier (un seul niveau de dossiers) */
views: [
{
type: "table",
label: "Fruits",
tableName: "fruits",
columns: [
{ label: "Nom", name: "fruitName" },
{ label: "Acidité", name: "acidity" },
],
},
],
},
]
注释:
- 文件夹只嵌入一等深处( S)
views无法包含其它文件夹 . roles编号 :readRoles在一个文件夹门上 整个小组的能见度, 就像他们在景色上一样.hidden: true在一个文件夹中隐藏整个组从侧边栏 分组视图仍可按其URL访问,如隐藏视图.
S3 上传文件
存在不同的列类型,其中之一是FileColumn,允许 上传一个文件到 AWS S3 。 基本配置如下:
...
columns: [{
label: 'Image',
name: 'imageUrl',
dataType: 'file',
s3Config: {
host: process.env.AWS_S3_HOST,
/** Optional. When omitted, the AWS SDK default credential chain is used. */
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_KEY,
bucket: process.env.AWS_S3_PODCAST_BUCKET_NAME,
/** Optional, only needed when your bucket isn't public and you need to generate a signed URL */
signedUrlNeeded: true,
/** Optional, only needed when your bucket isn't public and you need to generate a signed URL */
region: 'eu-west-3',
},
}]
何时 accessKeyId aWS SDK 将尝试解决证书本身( 第一局胜出) :
- Env vers AWS ACESS KEY ID + AWS SECRET ACESS KEY (+ AWS SESSION TOKEN),如果设定了AWS PROFILE,则跳过
- 配置文件 AWS PROFILE(或"默认"),来源于~/.aws/config和~/.aws/证书(SSO,静态密钥,所扮演的角色,证书 进程)
- 网络身份标志: AWS WEB IDENTITY Token FILE + AWS ROLE ARN(例如EKS) 互联网档案馆的存檔,存档日期2011-12-20
- ECS 容器作用: AWS Condenter Credentials relatical URI 或 AWS Contenterials FullL URI
- EC2 实例角色通过实例元数据服务, 除非 AWS EC2 METADATA DISABLED 被设定为任何除外的选项
false
建议依赖角色而不是在服务器上添加长寿的AWS证书:EC2/Elastic Beanstalk上的实例配置角色,或ECS/Fargate上的任务角色(不是执行角色). 为该角色添加以下权限 :
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:PutObjectAcl"
],
"Resource": "arn:aws:s3:::<bucket>/<subfolder>/*"
}]
}
- 无
subfolder,使用时arn:aws:s3:::<bucket>/*作为资源. - 默认情况下,文件上传为
public-readACL,所以桶必须允许ACLs:其对象所有权不能是"被执行的"(否则)AccessControlListNotSupported),以及BlockPublicAcls必须关闭( 否则AccessDenied) (中文(简体) ). - 与
signedUrlNeeded: true,没有设置ACL:仅限s3:PutObject需要,而桶可以保持隐私. - 如果桶被 SSE- KMS 加密, 也允许
kms:GenerateDataKey它的 KMS 密钥.
Google 云平台上的文件上传( GCP)
您也可以使用FileColumn上传文件到Google Cloud存储. 要使用GCP而不是AWS S3, 将 gcpConfig 配置如下 :
...
columns: [{
label: 'Image',
name: 'imageUrl',
dataType: 'file',
gcpConfig: {
host: process.env.GCP_STORAGE_HOST,
bucket: process.env.GCP_BUCKET_NAME,
},
}]
您还需要在您的服务账户中设置 .env 文件 :
GOOGLE_APPLICATION_CREDENTIALS=service-account-file.json
确保服务账户JSON文件在您的项目目录中,并拥有写入您Google Cloud存储桶的必要权限.
文件上传到远程服务器( 通过 SFTP)
可以使用FileColumn允许通过SFTP上传到远程服务器. 基本配置如下:
...
columns: [{
label: 'Image',
name: 'imageUrl',
dataType: 'file',
publicPath: '/home/ubuntu/project/public',
subdirectory: 'img',
sftpConfig: {
host: process.env.SFTP_HOST,
user: process.env.SFTP_USER,
password: process.env.SFTP_PASSWORD,
},
}]
本地设置
本地游乐场,内有真实的 MySQL + PostgreSQL 容器,种子数据和 a 显示每个特性的演示配置 :
npm run playground:db && npm run playground:build
npm run playground:schema && npm run playground:seed
npm run playground # http://localhost:8080/admin/ (admin / admin)
见 playground/README.md 、涵盖的特征和
DB=pg|mysql|mysql2 后端开关 .
测试
后端有一个回归测试套件(Jest + ts-jest + supertest)覆盖 aPI 路由、授权矩阵、 MySQL 的 SQL 生成以及 PostgreSQL 方言、文件上传(S3/GCP/SFTP/local)、进出口和 登录/会话中间软件。 路由级别行为与真实数据库相悖 引擎(见下文);单元套件覆盖纯逻辑和配置接线 没有任何数据库.
npm run typecheck # check backend TypeScript without emitting build files
npm test # run the unit suite (no database required)
npm run test:watch # watch mode
npm run test:coverage # unit + integration merged report (Docker required)
实际数据库整合测试
第二套套间(tests/integration/) 运行相同的路线,以对抗真实的
(原始内容存档于2017-06-31) (中文(简体) ). MySQL 8 (简体) mysql 财务报告和审定财务报表 mysql2 postgreSQL 16服务器
开始于 测试容器它声称
持续状态, 不仅仅是释放 SQL, 而且需要一个运行中的 Docker 守护进程 .
npm run test:integration # TEST_INTEGRATION=1, jest.integration.config.js
npm run test:all # unit suite + integration suite
npm test 从不启动容器. npm run test:integration 需要一个
可达的 Docker 守护进程, 且没有它就失败了, 因此在 CI 中一个损坏的 Docker 设置
不能无声地通过.
这个 CI GitHub 动作工作流程运行排入、后端单元和集成
测试,以及牵引请求的前端测试。 部署工作流程重用
工作流程相同,只有在工作流程过后才能建立和出版。 要求这些
合并前的检查, 请按照目标中的要求配置 CI 测试检查
分支的保护规则.
已知的稳健性限制
前者 tests/security-known-gaps.test.ts 套件编码的未固定安全
作为预期的安全性测试,还存在强大的差距。 从那时起,它的所有差距都是
固定的,其测试被折叠成普通套房(routes.*.test.ts, (中文(简体) ),
dbManager.test.ts因此不需要特别跑步了 npm test 覆盖
他们.
仍然有记录表明存在一些强性限制(不能对HTTP进行断言) 因为错误作为未处理的拒绝在 express 4 中逃脱:
- 扔东西
parseImportFile内/recordsImport或者说/subrecordsImport实值 等待着多党召回的尝试/捕捉——这500人从不 发送和进程会崩溃. formatValueForInsertion(false)返回时"NULL",所以布尔false无法导入 .- 本地用户上传后端需要
subdirectory(公开Path +) 未定义 concat 生成一个不存在的上传目录 . - 扔东西
editCallbackFunction内/updateRecord正在等待外边的任何 尝试/捕获——更新是承诺的,但请求将永远搁置.
前端测试
反应战线( R)src/front)有一个小型烟雾套房(CRA / 反应标记)
) 覆盖纯的 utils , 外部点击处理器, 第一视图
重定向和未经认证的 App 渲染:
cd src/front && npm test -- --watchAll=false