|
| 1 | +export function gitignore() { |
| 2 | + return `# Dependency directories |
| 3 | +node_modules/ |
| 4 | +
|
| 5 | +# dotenv environment variable files |
| 6 | +.env |
| 7 | +`; |
| 8 | +} |
| 9 | + |
| 10 | +export function env(dbUrl, prismaDbUrl) { |
| 11 | + const base = ` |
| 12 | +ADMINFORTH_SECRET=123 |
| 13 | +NODE_ENV=development |
| 14 | +DATABASE_URL=${dbUrl} |
| 15 | +` |
| 16 | + if (prismaDbUrl) |
| 17 | + return base + `PRISMA_DATABASE_URL=${prismaDbUrl}`; |
| 18 | + return base; |
| 19 | +} |
| 20 | + |
| 21 | +export function envSample(dbUrl, prismaDbUrl) { |
| 22 | + return env(dbUrl, prismaDbUrl); |
| 23 | +} |
| 24 | + |
| 25 | +export function indexTs(appName) { |
| 26 | + return `import express from 'express'; |
| 27 | +import AdminForth, { Filters } from 'adminforth'; |
| 28 | +import usersResource from "./resources/users"; |
| 29 | +
|
| 30 | +const ADMIN_BASE_URL = ''; |
| 31 | +
|
| 32 | +export const admin = new AdminForth({ |
| 33 | + baseUrl : ADMIN_BASE_URL, |
| 34 | + auth: { |
| 35 | + usersResourceId: 'users', // resource to get user during login |
| 36 | + usernameField: 'email', // field where username is stored, should exist in resource |
| 37 | + passwordHashField: 'password_hash', |
| 38 | + rememberMeDays: 30, // users who will check "remember me" will stay logged in for 30 days |
| 39 | + loginBackgroundImage: 'https://images.unsplash.com/photo-1534239697798-120952b76f2b?q=80&w=3389&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D', |
| 40 | + loginBackgroundPosition: '1/2', // over, 3/4, 2/5, 3/5 (tailwind grid) |
| 41 | + demoCredentials: "adminforth:adminforth", // never use it for production |
| 42 | + loginPromptHTML: "Use email <b>adminforth</b> and password <b>adminforth</b> to login", |
| 43 | + }, |
| 44 | + customization: { |
| 45 | + brandName: '${appName}', |
| 46 | + title: '${appName}', |
| 47 | + favicon: '@@/assets/favicon.png', |
| 48 | + brandLogo: '@@/assets/logo.svg', |
| 49 | + datesFormat: 'DD MMM', |
| 50 | + timeFormat: 'HH:mm a', |
| 51 | + showBrandNameInSidebar: true, |
| 52 | + styles: { |
| 53 | + colors: { |
| 54 | + light: { |
| 55 | + // color for links, icons etc. |
| 56 | + primary: '#B400B8', |
| 57 | + // color for sidebar and text |
| 58 | + sidebar: {main:'#571E58', text:'white'}, |
| 59 | + }, |
| 60 | + dark: { |
| 61 | + primary: '#82ACFF', |
| 62 | + sidebar: {main:'#1f2937', text:'#9ca3af'}, |
| 63 | + } |
| 64 | + } |
| 65 | + }, |
| 66 | + }, |
| 67 | + dataSources: [ |
| 68 | + { |
| 69 | + id: 'maindb', |
| 70 | + url: \`\${process.env.DATABASE_URL}\` |
| 71 | + }, |
| 72 | + ], |
| 73 | + resources: [ |
| 74 | + usersResource, |
| 75 | + ], |
| 76 | + menu: [ |
| 77 | + { |
| 78 | + type: 'heading', |
| 79 | + label: 'SYSTEM', |
| 80 | + }, |
| 81 | + { |
| 82 | + label: 'Users', |
| 83 | + icon: 'flowbite:user-solid', // any icon from iconify supported in format <setname>:<icon>, e.g. from here https://icon-sets.iconify.design/flowbite/ |
| 84 | + resourceId: 'users', |
| 85 | + } |
| 86 | + ], |
| 87 | +}); |
| 88 | +
|
| 89 | +if (import.meta.url === \`file://\${process.argv[1]}\`) { |
| 90 | + // if script is executed directly e.g. node index.ts or npm start |
| 91 | +
|
| 92 | + const app = express() |
| 93 | + app.use(express.json()); |
| 94 | + const port = 3500; |
| 95 | +
|
| 96 | + // needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime |
| 97 | + await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'}); |
| 98 | + console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.'); |
| 99 | +
|
| 100 | + // serve after you added all api |
| 101 | + admin.express.serve(app) |
| 102 | +
|
| 103 | + admin.discoverDatabases().then(async () => { |
| 104 | + if (!await admin.resource('users').get([Filters.EQ('email', 'adminforth')])) { |
| 105 | + await admin.resource('users').create({ |
| 106 | + email: 'adminforth', |
| 107 | + password_hash: await AdminForth.Utils.generatePasswordHash('adminforth'), |
| 108 | + role: 'superadmin', |
| 109 | + }); |
| 110 | + } |
| 111 | + }); |
| 112 | +
|
| 113 | + admin.express.listen(port, () => { |
| 114 | + console.log(\`Example app listening at http://localhost:\${port}\`) |
| 115 | + console.log(\`\\n⚡ AdminForth is available at http://localhost:\${port}\${ADMIN_BASE_URL}\n\`) |
| 116 | + }); |
| 117 | +} |
| 118 | +`; |
| 119 | +} |
| 120 | + |
| 121 | +export function schemaPrisma(provider, dbUrl) { |
| 122 | + return `generator client { |
| 123 | + provider = "prisma-client-js" |
| 124 | +} |
| 125 | +
|
| 126 | +datasource db { |
| 127 | + provider = "${provider}" |
| 128 | + url = env("PRISMA_DATABASE_URL") |
| 129 | +} |
| 130 | +
|
| 131 | +model adminuser { |
| 132 | + id String @id |
| 133 | + email String @unique |
| 134 | + password_hash String |
| 135 | + role String |
| 136 | + created_at DateTime |
| 137 | +} |
| 138 | +`; |
| 139 | +} |
| 140 | + |
| 141 | +export function usersResource() { |
| 142 | + return `import AdminForth, { AdminForthDataTypes, AdminForthResourceInput } from 'adminforth'; |
| 143 | +import type { AdminUser } from 'adminforth'; |
| 144 | +
|
| 145 | +async function canModifyUsers({ adminUser }: { adminUser: AdminUser }): Promise<boolean> { |
| 146 | + return adminUser.dbUser.role === 'superadmin'; |
| 147 | +} |
| 148 | +
|
| 149 | +export default { |
| 150 | + dataSource: 'maindb', |
| 151 | + table: 'adminuser', |
| 152 | + resourceId: 'users', |
| 153 | + label: 'Users', |
| 154 | + recordLabel: (r) => \`👤 \${r.email}\`, |
| 155 | + options: { |
| 156 | + allowedActions: { |
| 157 | + edit: canModifyUsers, |
| 158 | + delete: canModifyUsers, |
| 159 | + }, |
| 160 | + }, |
| 161 | + columns: [ |
| 162 | + { |
| 163 | + name: 'id', |
| 164 | + primaryKey: true, |
| 165 | + type: AdminForthDataTypes.STRING, |
| 166 | + fillOnCreate: ({ initialRecord, adminUser }) => Math.random().toString(36).substring(7), |
| 167 | + showIn: ['list', 'filter', 'show'], |
| 168 | + }, |
| 169 | + { |
| 170 | + name: 'email', |
| 171 | + required: true, |
| 172 | + isUnique: true, |
| 173 | + type: AdminForthDataTypes.STRING, |
| 174 | + validation: [ |
| 175 | + // you can also use AdminForth.Utils.EMAIL_VALIDATOR which is alias to this object |
| 176 | + { |
| 177 | + regExp: '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$', |
| 178 | + message: 'Email is not valid, must be in format example@test.com' |
| 179 | + }, |
| 180 | + ] |
| 181 | + }, |
| 182 | + { |
| 183 | + name: 'created_at', |
| 184 | + type: AdminForthDataTypes.DATETIME, |
| 185 | + showIn: ['list', 'filter', 'show'], |
| 186 | + fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(), |
| 187 | + }, |
| 188 | + { |
| 189 | + name: 'role', |
| 190 | + type: AdminForthDataTypes.STRING, |
| 191 | + enum: [ |
| 192 | + { value: 'superadmin', label: 'Super Admin' }, |
| 193 | + { value: 'user', label: 'User' }, |
| 194 | + ] |
| 195 | + }, |
| 196 | + { |
| 197 | + name: 'password', |
| 198 | + virtual: true, // field will not be persisted into db |
| 199 | + required: { create: true }, // make required only on create page |
| 200 | + editingNote: { edit: 'Leave empty to keep password unchanged' }, |
| 201 | + type: AdminForthDataTypes.STRING, |
| 202 | + showIn: ['create', 'edit'], // to show field only on create and edit pages |
| 203 | + masked: true, // to show stars in input field |
| 204 | +
|
| 205 | + minLength: 8, |
| 206 | + validation: [ |
| 207 | + // request to have at least 1 digit, 1 upper case, 1 lower case |
| 208 | + AdminForth.Utils.PASSWORD_VALIDATORS.UP_LOW_NUM, |
| 209 | + ], |
| 210 | + }, |
| 211 | + { |
| 212 | + name: 'password_hash', |
| 213 | + type: AdminForthDataTypes.STRING, |
| 214 | + backendOnly: true, |
| 215 | + showIn: [] |
| 216 | + } |
| 217 | + ], |
| 218 | + hooks: { |
| 219 | + create: { |
| 220 | + beforeSave: async ({ record, adminUser, resource }) => { |
| 221 | + record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password); |
| 222 | + return { ok: true }; |
| 223 | + } |
| 224 | + }, |
| 225 | + edit: { |
| 226 | + beforeSave: async ({ record, adminUser, resource }) => { |
| 227 | + if (record.password) { |
| 228 | + record.password_hash = await AdminForth.Utils.generatePasswordHash(record.password); |
| 229 | + } |
| 230 | + return { ok: true } |
| 231 | + }, |
| 232 | + }, |
| 233 | + } |
| 234 | +} as AdminForthResourceInput; |
| 235 | +`; |
| 236 | +} |
| 237 | + |
| 238 | +/** |
| 239 | + * Root package.json template |
| 240 | + */ |
| 241 | +export function rootPackageJson(appName) { |
| 242 | + // Note: you might want to keep versions in sync or fetch from a config |
| 243 | + return `{ |
| 244 | + "name": "${appName}", |
| 245 | + "version": "1.0.0", |
| 246 | + "main": "index.ts", |
| 247 | + "type": "module", |
| 248 | + "keywords": [], |
| 249 | + "author": "", |
| 250 | + "license": "ISC", |
| 251 | + "description": "", |
| 252 | + "scripts": { |
| 253 | + "start": "tsx watch --env-file=.env index.ts", |
| 254 | + "migrate": "npx prisma migrate deploy", |
| 255 | + "makemigration": "npx --yes prisma migrate deploy; npx --yes prisma migrate dev", |
| 256 | + "test": "echo \\"Error: no test specified\\" && exit 1" |
| 257 | + }, |
| 258 | + "engines": { |
| 259 | + "node": ">=20" |
| 260 | + }, |
| 261 | + "dependencies": { |
| 262 | + "adminforth": "latest", |
| 263 | + "express": "latest" |
| 264 | + }, |
| 265 | + "devDependencies": { |
| 266 | + "typescript": "5.4.5", |
| 267 | + "tsx": "4.11.2", |
| 268 | + "@types/express": "latest", |
| 269 | + "@types/node": "latest" |
| 270 | + } |
| 271 | +} |
| 272 | +`; |
| 273 | +} |
| 274 | + |
| 275 | +/** |
| 276 | + * Root tsconfig.json template |
| 277 | + */ |
| 278 | +export function rootTsConfig() { |
| 279 | + return `{ |
| 280 | + "compilerOptions": { |
| 281 | + "target": "esnext", |
| 282 | + "module": "nodenext", |
| 283 | + "esModuleInterop": true, |
| 284 | + "forceConsistentCasingInFileNames": true, |
| 285 | + "strict": true |
| 286 | + }, |
| 287 | + "exclude": ["node_modules", "dist"] |
| 288 | +} |
| 289 | +`; |
| 290 | +} |
| 291 | + |
| 292 | +/** |
| 293 | + * custom/package.json |
| 294 | + */ |
| 295 | +export function customPackageJson(appName) { |
| 296 | + return `{ |
| 297 | + "name": "custom", |
| 298 | + "version": "1.0.0", |
| 299 | + "main": "index.ts", |
| 300 | + "scripts": { |
| 301 | + "test": "echo \\"Error: no test specified\\" && exit 1" |
| 302 | + }, |
| 303 | + "keywords": [], |
| 304 | + "author": "", |
| 305 | + "license": "ISC", |
| 306 | + "description": "" |
| 307 | +} |
| 308 | +`; |
| 309 | +} |
| 310 | + |
| 311 | +/** |
| 312 | + * custom/tsconfig.json |
| 313 | + */ |
| 314 | +export function customTsconfig() { |
| 315 | + return `{ |
| 316 | + "compilerOptions": { |
| 317 | + "baseUrl": ".", |
| 318 | + "paths": { |
| 319 | + "@/": "../node_modules/adminforth/dist/spa/src/", |
| 320 | + "": "../node_modules/adminforth/dist/spa/node_modules/", |
| 321 | + "@@/*": "." |
| 322 | + } |
| 323 | + } |
| 324 | +} |
| 325 | +`; |
| 326 | +} |
0 commit comments