diff --git a/adapters/install-adapters.sh b/adapters/install-adapters.sh index f6ccadb1..965c74b1 100644 --- a/adapters/install-adapters.sh +++ b/adapters/install-adapters.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses adminforth-google-oauth-adapter adminforth-github-oauth-adapter adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter adminforth-image-generation-adapter-openai adminforth-storage-adapter-amazon-s3 adminforth-storage-adapter-local" +ADAPTERS="adminforth-completion-adapter-open-ai-chat-gpt adminforth-email-adapter-aws-ses adminforth-google-oauth-adapter adminforth-github-oauth-adapter adminforth-facebook-oauth-adapter adminforth-keycloak-oauth-adapter adminforth-microsoft-oauth-adapter adminforth-twitch-oauth-adapter adminforth-image-generation-adapter-openai adminforth-storage-adapter-amazon-s3 adminforth-storage-adapter-local" # for each install_adapter() { diff --git a/adminforth/commands/createApp/templates/index.ts.hbs b/adminforth/commands/createApp/templates/index.ts.hbs index 9fd82d17..6d6bf096 100644 --- a/adminforth/commands/createApp/templates/index.ts.hbs +++ b/adminforth/commands/createApp/templates/index.ts.hbs @@ -1,6 +1,8 @@ import express from 'express'; import AdminForth from 'adminforth'; import usersResource from "./resources/adminuser.js"; +import { fileURLToPath } from 'url'; +import path from 'path'; const ADMIN_BASE_URL = ''; @@ -55,7 +57,7 @@ export const admin = new AdminForth({ ], }); -if (import.meta.url === `file://${process.argv[1]}`) { +if (fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { const app = express(); app.use(express.json()); diff --git a/adminforth/commands/createApp/utils.js b/adminforth/commands/createApp/utils.js index 9d311fc9..6bd99ef6 100644 --- a/adminforth/commands/createApp/utils.js +++ b/adminforth/commands/createApp/utils.js @@ -284,14 +284,22 @@ async function writeTemplateFiles(dirname, cwd, options) { } async function installDependencies(ctx, cwd) { - const nodeBinary = process.execPath; // Path to the Node.js binary running this script - const npmPath = path.join(path.dirname(nodeBinary), 'npm'); // Path to the npm executable + const isWindows = process.platform === 'win32'; + const nodeBinary = process.execPath; + const npmPath = path.join(path.dirname(nodeBinary), 'npm'); const customDir = ctx.customDir; - const res = await Promise.all([ - await execAsync(`${nodeBinary} ${npmPath} install`, { cwd, env: { PATH: process.env.PATH } }), - await execAsync(`${nodeBinary} ${npmPath} install`, { cwd: customDir, env: { PATH: process.env.PATH } }), - ]); + if (isWindows) { + const res = await Promise.all([ + await execAsync(`npm install`, { cwd, env: { PATH: process.env.PATH } }), + await execAsync(`npm install`, { cwd: customDir, env: { PATH: process.env.PATH } }), + ]); + } else { + const res = await Promise.all([ + await execAsync(`${nodeBinary} ${npmPath} install`, { cwd, env: { PATH: process.env.PATH } }), + await execAsync(`${nodeBinary} ${npmPath} install`, { cwd: customDir, env: { PATH: process.env.PATH } }), + ]); + } // console.log(chalk.dim(`Dependencies installed in ${cwd} and ${customDir}: \n${res[0].stdout}${res[1].stdout}`)); } diff --git a/adminforth/commands/createCustomComponent/configUpdater.js b/adminforth/commands/createCustomComponent/configUpdater.js index 600a3fcf..2ed43e1b 100644 --- a/adminforth/commands/createCustomComponent/configUpdater.js +++ b/adminforth/commands/createCustomComponent/configUpdater.js @@ -254,7 +254,7 @@ export async function updateResourceConfig(resourceId, columnName, fieldType, co } -export async function injectLoginComponent(indexFilePath, componentPath) { +export async function injectLoginComponent(indexFilePath, componentPath, injectionType) { console.log(chalk.dim(`Reading file: ${indexFilePath}`)); const content = await fs.readFile(indexFilePath, 'utf-8'); const ast = recast.parse(content, { @@ -263,6 +263,7 @@ export async function injectLoginComponent(indexFilePath, componentPath) { let updated = false; let injectionLine = null; + let targetProperty = null; recast.visit(ast, { visitNewExpression(path) { @@ -293,20 +294,23 @@ export async function injectLoginComponent(indexFilePath, componentPath) { const loginPageInjections = getOrCreateProp(customization, 'loginPageInjections'); if (!n.ObjectExpression.check(loginPageInjections)) return false; - let underInputsProp = loginPageInjections.properties.find( - p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === 'underInputs' + // Determine target property based on injection type + targetProperty = injectionType === 'beforeLogin' ? 'panelHeader' : 'underInputs'; + + let targetProp = loginPageInjections.properties.find( + p => n.ObjectProperty.check(p) && n.Identifier.check(p.key) && p.key.name === targetProperty ); - if (underInputsProp) { - const currentVal = underInputsProp.value; - injectionLine = underInputsProp.loc?.start.line ?? null; + if (targetProp) { + const currentVal = targetProp.value; + injectionLine = targetProp.loc?.start.line ?? null; if (n.StringLiteral.check(currentVal)) { if (currentVal.value !== componentPath) { - underInputsProp.value = b.arrayExpression([ + targetProp.value = b.arrayExpression([ b.stringLiteral(currentVal.value), b.stringLiteral(componentPath), ]); - console.log(chalk.dim(`Converted 'underInputs' to array with existing + new path.`)); + console.log(chalk.dim(`Converted '${targetProperty}' to array with existing + new path.`)); } else { console.log(chalk.dim(`Component path already present as string. Skipping.`)); } @@ -316,26 +320,26 @@ export async function injectLoginComponent(indexFilePath, componentPath) { ); if (!exists) { currentVal.elements.push(b.stringLiteral(componentPath)); - console.log(chalk.dim(`Appended new component path to existing 'underInputs' array.`)); + console.log(chalk.dim(`Appended new component path to existing '${targetProperty}' array.`)); } else { console.log(chalk.dim(`Component path already present in array. Skipping.`)); } } else { - console.warn(chalk.yellow(`⚠️ 'underInputs' is not a string or array. Skipping.`)); + console.warn(chalk.yellow(`⚠️ '${targetProperty}' is not a string or array. Skipping.`)); return false; } } else { const newProperty = b.objectProperty( - b.identifier('underInputs'), - b.stringLiteral(componentPath) - ); - - if (newProperty.loc) { - console.log(chalk.dim(`Adding 'underInputs' at line: ${newProperty.loc.start.line}`)); - } - - loginPageInjections.properties.push(newProperty); - console.log(chalk.dim(`Added 'underInputs': ${componentPath}`)); + b.identifier(targetProperty), + b.stringLiteral(componentPath) + ); + + if (newProperty.loc) { + console.log(chalk.dim(`Adding '${targetProperty}' at line: ${newProperty.loc.start.line}`)); + } + + loginPageInjections.properties.push(newProperty); + console.log(chalk.dim(`Added '${targetProperty}': ${componentPath}`)); } updated = true; @@ -353,7 +357,7 @@ export async function injectLoginComponent(indexFilePath, componentPath) { await fs.writeFile(indexFilePath, outputCode, 'utf-8'); console.log( chalk.green( - `✅ Successfully updated CRUD injection in resource file: ${indexFilePath}` + + `✅ Successfully updated login ${targetProperty} injection in: ${indexFilePath}` + (injectionLine !== null ? `:${injectionLine}` : '') ) ); diff --git a/adminforth/commands/createCustomComponent/fileGenerator.js b/adminforth/commands/createCustomComponent/fileGenerator.js index 7da934c8..d6c281d6 100644 --- a/adminforth/commands/createCustomComponent/fileGenerator.js +++ b/adminforth/commands/createCustomComponent/fileGenerator.js @@ -129,7 +129,7 @@ export async function generateLoginOrGlobalComponentFile(componentFileName, inje const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); let templatePath; - if (injectionType === 'afterLogin') { + if (injectionType === 'afterLogin' || injectionType === 'beforeLogin') { templatePath = path.join(__dirname, 'templates', 'login', `${injectionType}.vue.hbs`); } else { templatePath = path.join(__dirname, 'templates', 'global', `${injectionType}.vue.hbs`); diff --git a/adminforth/commands/createCustomComponent/main.js b/adminforth/commands/createCustomComponent/main.js index e9236bf2..76c0ef1f 100644 --- a/adminforth/commands/createCustomComponent/main.js +++ b/adminforth/commands/createCustomComponent/main.js @@ -50,6 +50,7 @@ async function handleFieldComponentCreation(config, resources) { { name: '📃 show', value: 'show' }, { name: '✏️ edit', value: 'edit' }, { name: '➕ create', value: 'create' }, + { name: '🔍 filter', value: 'filter'}, new Separator(), { name: '🔙 BACK', value: '__BACK__' }, ] @@ -256,6 +257,7 @@ async function handleLoginPageInjectionCreation(config) { const injectionType = await select({ message: 'Select injection type:', choices: [ + { name: 'Before Login and password inputs', value: 'beforeLogin' }, { name: 'After Login and password inputs', value: 'afterLogin' }, { name: '🔙 BACK', value: '__BACK__' }, ], @@ -286,7 +288,7 @@ async function handleLoginPageInjectionCreation(config) { console.log(chalk.dim(`Component generation successful: ${absoluteComponentPath}`)); const configFilePath = path.resolve(process.cwd(), 'index.ts'); console.log(chalk.dim(`Injecting component: ${configFilePath}, ${componentFileName}`)); - await injectLoginComponent(configFilePath, `@@/${componentFileName}`); + await injectLoginComponent(configFilePath, `@@/${componentFileName}`, injectionType); console.log( chalk.bold.greenBright('You can now open the component in your IDE:'), diff --git a/adminforth/commands/createCustomComponent/templates/customFields/filter.vue.hbs b/adminforth/commands/createCustomComponent/templates/customFields/filter.vue.hbs new file mode 100644 index 00000000..9a2876be --- /dev/null +++ b/adminforth/commands/createCustomComponent/templates/customFields/filter.vue.hbs @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/adminforth/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs b/adminforth/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs new file mode 100644 index 00000000..8e5ee197 --- /dev/null +++ b/adminforth/commands/createCustomComponent/templates/login/beforeLogin.vue.hbs @@ -0,0 +1,18 @@ + + + + diff --git a/adminforth/commands/postinstall.js b/adminforth/commands/postinstall.js new file mode 100644 index 00000000..23e21cd3 --- /dev/null +++ b/adminforth/commands/postinstall.js @@ -0,0 +1,15 @@ +import fs from 'fs'; +import path from 'path'; + +import { execSync } from 'child_process'; +const spaPath = path.join(import.meta.dirname, 'dist', 'spa'); + + +if (fs.existsSync(spaPath)){ + console.log('Installing SPA dependencies...'); + execSync('npm ci', { cwd: spaPath, stdio: 'inherit' }); + console.log('Installed spa dependencies'); +} else { + console.log('SPA dependencies not found'); + console.log('current directory', import.meta.dirname); +} \ No newline at end of file diff --git a/adminforth/documentation/blog/2024-10-01-ai-blog/index.md b/adminforth/documentation/blog/2024-10-01-ai-blog/index.md index 1315e70e..abdfc75d 100644 --- a/adminforth/documentation/blog/2024-10-01-ai-blog/index.md +++ b/adminforth/documentation/blog/2024-10-01-ai-blog/index.md @@ -199,7 +199,7 @@ model ContentImage { Create a migration: ```bash -npm run makemigration -- --name add-posts +npm run makemigration -- --name add-posts && npm run migrate:local ``` diff --git a/adminforth/documentation/docs/tutorial/001-gettingStarted.md b/adminforth/documentation/docs/tutorial/001-gettingStarted.md index 7a8cc336..45f48904 100644 --- a/adminforth/documentation/docs/tutorial/001-gettingStarted.md +++ b/adminforth/documentation/docs/tutorial/001-gettingStarted.md @@ -89,7 +89,7 @@ This will create a migration file in `migrations` and apply it to the database. In future, when you need to add new resources, you need to modify `schema.prisma` (add models, change fields, etc.). After doing any modification you need to create a new migration using next command: ```bash -npm run makemigration -- --name +npm run makemigration -- --name init && npm run migrate:local ``` Other developers need to pull migration and run `npm run migrateLocal` to apply any unapplied migrations. @@ -173,7 +173,7 @@ model apartments { Run the following command to create a new migration: ```bash -npm run makemigration -- --name add-apartments +npm run makemigration -- --name add-apartments && npm run migrate:local ``` ### Step3. Create the `apartments` resource diff --git a/adminforth/documentation/docs/tutorial/01-helloWorld.md b/adminforth/documentation/docs/tutorial/01-helloWorld.md index 756c1f0d..65c3b59b 100644 --- a/adminforth/documentation/docs/tutorial/01-helloWorld.md +++ b/adminforth/documentation/docs/tutorial/01-helloWorld.md @@ -122,7 +122,7 @@ model Post { Create database using `prisma migrate`: ```bash -npm run makemigration --name init +npm run makemigration --name init && npm run migrate:local ``` ## Setting up AdminForth diff --git a/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md b/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md index 96d04a6b..0fdd2b51 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/02-customFieldRendering.md @@ -481,4 +481,119 @@ list: '@/renderers/ZeroStylesRichText.vue', //diff-add ``` -`ZeroStyleRichText` fits well for tasks like email templates preview fields. \ No newline at end of file +`ZeroStyleRichText` fits well for tasks like email templates preview fields. + + +### Custom filter component for square meters + + +Sometimes standard filters are not enough, and you want to make a convenient UI for selecting a range of apartment areas. For example, buttons with options for “Small (<25 m²)”, “Medium (25–90 m²)” and “Large (>90 m²)”. + +```ts title='./custom/SquareMetersFilter.vue' + + + +``` + +```ts title='./resources/apartments.ts' + columns: [ + ... + { + name: 'square_meter', + label: 'Square', + //diff-add + components: { + //diff-add + filter: '@@/SquareMetersFilter.vue' + //diff-add + } + }, + ... +] \ No newline at end of file diff --git a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md index 2375c8a9..df662069 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/08-pageInjections.md @@ -141,9 +141,9 @@ Here is how it looks: You can also inject custom components to the login page. -`loginPageInjections.underInputs` allows to add one or more panels under the login form inputs: +`loginPageInjections.underInputs` and `loginPageInjections.panelHeader` allows to add one or more panels under or over the login form inputs: -![login Page Injections underInputs]() +![login Page Injections underInputs]() For example: @@ -172,6 +172,38 @@ Now create file `CustomLoginFooter.vue` in the `custom` folder of your project: ``` +Also you can add `panelHeader` + +```ts title="/index.ts" + +new AdminForth({ + ... + customization: { + loginPageInjections: { + underInputs: '@@/CustomLoginFooter.vue', +//diff-add + panelHeader: '@@/CustomLoginHeader.vue', + } + ... + } + + ... +}) +``` + +Now create file `CustomLoginHeader.vue` in the `custom` folder of your project: + +```html title="./custom/CustomLoginHeader.vue" + +``` + + ## List view page injections shrinking: thin enough to shrink? diff --git a/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md b/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md index fb0e7d67..dfbd7909 100644 --- a/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md +++ b/adminforth/documentation/docs/tutorial/03-Customization/15-afcl.md @@ -240,6 +240,7 @@ import { Input } from '@/afcl' ``` +
![AFCL Input](image-46.png) @@ -247,6 +248,29 @@ import { Input } from '@/afcl'
+
+
+```js +import { Input } from '@/afcl' +import { IconSearchOutline } from '@iconify-prerendered/vue-flowbite' +``` + + +```html + + + +``` + + +
+
+ ![AFCL Input](inputRightIcon.png) +
+
+ ## Tooltip Wrap an element on which you would like to show a tooltip with the `Tooltip` component and add a `tooltip` slot to it. diff --git a/adminforth/documentation/docs/tutorial/03-Customization/inputRightIcon.png b/adminforth/documentation/docs/tutorial/03-Customization/inputRightIcon.png new file mode 100644 index 00000000..a613c576 Binary files /dev/null and b/adminforth/documentation/docs/tutorial/03-Customization/inputRightIcon.png differ diff --git a/adminforth/documentation/docs/tutorial/03-Customization/loginPageInjection.png b/adminforth/documentation/docs/tutorial/03-Customization/loginPageInjection.png new file mode 100644 index 00000000..c3d15377 Binary files /dev/null and b/adminforth/documentation/docs/tutorial/03-Customization/loginPageInjection.png differ diff --git a/adminforth/documentation/docs/tutorial/05-Plugins/04-RichEditor.md b/adminforth/documentation/docs/tutorial/05-Plugins/04-RichEditor.md index be2fd04f..92b35e69 100644 --- a/adminforth/documentation/docs/tutorial/05-Plugins/04-RichEditor.md +++ b/adminforth/documentation/docs/tutorial/05-Plugins/04-RichEditor.md @@ -159,7 +159,7 @@ model description_image { ``` ```bash -npm run makemigration -- --name add_description_image +npm run makemigration -- --name add_description_image && npm run migrate:local ``` ```bash @@ -167,10 +167,10 @@ npm i @adminforth/upload --save npm i @adminforth/storage-adapter-local --save ``` -```typescript title="./resources/description_image.ts" -import AdminForthStorageAdapterLocalFilesystem from "../../adapters/adminforth-storage-adapter-local"; -import { AdminForthResourceInput } from "../../adminforth"; -import UploadPlugin from "../../plugins/adminforth-upload"; +```typescript title="./resources/description_images.ts" +import AdminForthStorageAdapterLocalFilesystem from "@adminforth/storage-adapter-local"; +import { AdminForthResourceInput } from "adminforth"; +import UploadPlugin from "@adminforth/upload"; import { v1 as uuid } from "uuid"; export default { @@ -237,6 +237,20 @@ export default { ], } as AdminForthResourceInput; ``` +Next, add new resource to `index.ts`: + +```typescript title="./index.ts" +import descriptionImage from './resources/description_images.js'; + +... + + resources: [ + usersResource, + apartments, + // diff-add + descriptionImage + ], +``` Next, add attachments to RichEditor plugin: diff --git a/adminforth/documentation/docs/tutorial/05-Plugins/11-oauth.md b/adminforth/documentation/docs/tutorial/05-Plugins/11-oauth.md index 9ec8361b..3504197e 100644 --- a/adminforth/documentation/docs/tutorial/05-Plugins/11-oauth.md +++ b/adminforth/documentation/docs/tutorial/05-Plugins/11-oauth.md @@ -299,6 +299,44 @@ plugins: [ ] ``` +### Twitch Adapter + +Install Adapter: + +``` +npm install @adminforth/twitch-oauth-adapter --save +``` + +1. Go to the [Twitch dashboard](https://dev.twitch.tv/console/) +2. Create a new app or select an existing one +3. In `OAuth Redirect URLs` add `https://your-domain/oauth/callback` (`http://localhost:3500/oauth/callback`) +4. Go to the app and copy `Client ID`, click to `Generate a new client secret`(in Twitch this button can be used only once for some time, becouse of this dont lose it) button and copy secret . +5. Add the credentials to your `.env` file: + +```bash +TWITCH_OAUTH_CLIENT_ID=your_twitch_client_id +TWITCH_OAUTH_CLIENT_SECRET=your_twitch_client_secret +``` + +Add the adapter to your plugin configuration: + +```typescript title="./resources/adminuser.ts" +import AdminForthAdapterTwitchOauth2 from '@adminforth/twitch-oauth-adapter'; + +// ... existing resource configuration ... +plugins: [ + new OAuthPlugin({ + adapters: [ + ... + new AdminForthAdapterTwitchOauth2({ + clientID: process.env.TWITCH_OAUTH_CLIENT_ID, + clientSecret: process.env.TWITCH_OAUTH_CLIENT_SECRET, + }), + ], + }), +] +``` + ### Need custom provider? Just fork any existing adapter e.g. [Google](https://github.com/devforth/adminforth-google-oauth-adapter) and adjust it to your needs. diff --git a/adminforth/documentation/docs/tutorial/05-Plugins/14-markdown.md b/adminforth/documentation/docs/tutorial/05-Plugins/14-markdown.md index 949fa038..54d523b6 100644 --- a/adminforth/documentation/docs/tutorial/05-Plugins/14-markdown.md +++ b/adminforth/documentation/docs/tutorial/05-Plugins/14-markdown.md @@ -51,7 +51,7 @@ model description_image { ``` ```bash -npm run makemigration -- --name add_description_image +npm run makemigration -- --name add_description_image && npm run migrate:local ``` ```bash @@ -59,10 +59,10 @@ npm i @adminforth/upload --save npm i @adminforth/storage-adapter-local --save ``` -```typescript title="./resources/description_image.ts" -import AdminForthStorageAdapterLocalFilesystem from "../../adapters/adminforth-storage-adapter-local"; -import { AdminForthResourceInput } from "../../adminforth"; -import UploadPlugin from "../../plugins/adminforth-upload"; +```typescript title="./resources/description_images.ts" +import AdminForthStorageAdapterLocalFilesystem from "@adminforth/storage-adapter-local"; +import { AdminForthResourceInput } from "adminforth"; +import UploadPlugin from "@adminforth/upload"; import { v1 as uuid } from "uuid"; export default { @@ -129,6 +129,21 @@ export default { ], } as AdminForthResourceInput; ``` +Next, add new resource to `index.ts`: + +```typescript title="./index.ts" +// diff-add +import descriptionImage from './resources/description_images.js'; + +... + + resources: [ + usersResource, + apartments, + // diff-add + descriptionImage + ], +``` Next, add attachments to Markdown plugin: @@ -139,7 +154,7 @@ import MarkdownPlugin from '@adminforth/markdown'; plugins: [ new MarkdownPlugin({ - fieldName: "description" + fieldName: "description", // diff-add attachments: { // diff-add diff --git a/adminforth/index.ts b/adminforth/index.ts index 2211b380..e11b3bcb 100644 --- a/adminforth/index.ts +++ b/adminforth/index.ts @@ -124,31 +124,58 @@ class AdminForth implements IAdminForth { } constructor(config: AdminForthInputConfig) { + process.env.HEAVY_DEBUG && console.log('🔧 AdminForth constructor started'); + if (global.adminforth) { throw new Error('AdminForth instance already created in this process. '+ 'If you want to use multiple instances, consider using different process for each instance'); } + + process.env.HEAVY_DEBUG && console.log('🔧 Creating CodeInjector...'); this.codeInjector = new CodeInjector(this); + process.env.HEAVY_DEBUG && console.log('🔧 CodeInjector created'); + + process.env.HEAVY_DEBUG && console.log('🔧 Creating ConfigValidator...'); this.configValidator = new ConfigValidator(this, config); + process.env.HEAVY_DEBUG && console.log('🔧 ConfigValidator created'); + + process.env.HEAVY_DEBUG && console.log('🔧 Creating AdminForthRestAPI...'); this.restApi = new AdminForthRestAPI(this); + process.env.HEAVY_DEBUG && console.log('🔧 AdminForthRestAPI created'); + + process.env.HEAVY_DEBUG && console.log('🔧 Creating SocketBroker...'); this.websocket = new SocketBroker(this); + process.env.HEAVY_DEBUG && console.log('🔧 SocketBroker created'); + this.activatedPlugins = []; + process.env.HEAVY_DEBUG && console.log('🔧 Validating config...'); this.configValidator.validateConfig(); + process.env.HEAVY_DEBUG && console.log('🔧 Config validated'); + + process.env.HEAVY_DEBUG && console.log('🔧 Activating plugins...'); this.activatePlugins(); + process.env.HEAVY_DEBUG && console.log('🔧 Plugins activated'); + process.env.HEAVY_DEBUG && console.log('🔧 Creating ExpressServer...'); this.express = new ExpressServer(this); + process.env.HEAVY_DEBUG && console.log('🔧 ExpressServer created'); + // this.fastify = new FastifyServer(this); + process.env.HEAVY_DEBUG && console.log('🔧 Creating AdminForthAuth...'); this.auth = new AdminForthAuth(this); + process.env.HEAVY_DEBUG && console.log('🔧 AdminForthAuth created'); + this.connectors = {}; this.statuses = { dbDiscover: 'running', }; - - console.log(`${this.formatAdminForth()} v${ADMINFORTH_VERSION} initializing...`); + process.env.HEAVY_DEBUG && console.log('🔧 About to set global.adminforth...'); global.adminforth = this; + process.env.HEAVY_DEBUG && console.log('🔧 global.adminforth set successfully'); + process.env.HEAVY_DEBUG && console.log('🔧 AdminForth constructor completed'); } formatAdminForth() { @@ -174,14 +201,21 @@ class AdminForth implements IAdminForth { process.env.HEAVY_DEBUG && console.log('🔌🔌🔌 Activating plugins'); const allPluginInstances = []; for (let resource of this.config.resources) { + process.env.HEAVY_DEBUG && console.log(`🔌 Checking plugins for resource: ${resource.resourceId}`); for (let pluginInstance of resource.plugins || []) { + process.env.HEAVY_DEBUG && console.log(`🔌 Found plugin: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`); allPluginInstances.push({pi: pluginInstance, resource}); } } + process.env.HEAVY_DEBUG && console.log(`🔌 Total plugins to activate: ${allPluginInstances.length}`); allPluginInstances.sort(({pi: a}, {pi: b}) => a.activationOrder - b.activationOrder); + allPluginInstances.forEach( - ({pi: pluginInstance, resource}) => { + ({pi: pluginInstance, resource}, index) => { + process.env.HEAVY_DEBUG && console.log(`🔌 Activating plugin ${index + 1}/${allPluginInstances.length}: ${pluginInstance.constructor.name} for resource ${resource.resourceId}`); pluginInstance.modifyResourceConfig(this, resource); + process.env.HEAVY_DEBUG && console.log(`🔌 Plugin ${pluginInstance.constructor.name} modifyResourceConfig completed`); + const plugin = this.activatedPlugins.find((p) => p.pluginInstanceId === pluginInstance.pluginInstanceId); if (plugin) { process.env.HEAVY_DEBUG && console.log(`Current plugin pluginInstance.pluginInstanceId ${pluginInstance.pluginInstanceId}`); @@ -190,8 +224,10 @@ class AdminForth implements IAdminForth { To support multiple plugin instance pre one resource, plugin should return unique string values for each installation from instanceUniqueRepresentation`); } this.activatedPlugins.push(pluginInstance); + process.env.HEAVY_DEBUG && console.log(`🔌 Plugin ${pluginInstance.constructor.name} activated successfully`); } ); + process.env.HEAVY_DEBUG && console.log('🔌 All plugins activation completed'); } getPluginsByClassName(className: string): T[] { diff --git a/adminforth/modules/codeInjector.ts b/adminforth/modules/codeInjector.ts index b54ba9db..c64507a1 100644 --- a/adminforth/modules/codeInjector.ts +++ b/adminforth/modules/codeInjector.ts @@ -16,7 +16,11 @@ let TMP_DIR; try { TMP_DIR = os.tmpdir(); } catch (e) { - TMP_DIR = '/tmp'; //maybe we can consider to use node_modules/.cache/adminforth here instead of tmp + if (process.platform === 'win32') { + TMP_DIR = process.env.TEMP || process.env.TMP || 'C:\\Windows\\Temp'; + } else { + TMP_DIR = '/tmp'; + }//maybe we can consider to use node_modules/.cache/adminforth here instead of tmp } function stripAnsiCodes(str) { @@ -112,7 +116,9 @@ class CodeInjector implements ICodeInjector { envOverrides?: { [key: string]: string } }) { const nodeBinary = process.execPath; // Path to the Node.js binary running this script - const npmPath = path.join(path.dirname(nodeBinary), 'npm'); // Path to the npm executable + // On Windows, npm is npm.cmd, on Unix systems it's npm + const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const npmPath = path.join(path.dirname(nodeBinary), npmExecutable); // Path to the npm executable const env = { VITE_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl, FORCE_COLOR: '1', @@ -123,10 +129,31 @@ class CodeInjector implements ICodeInjector { console.log(`⚙️ exec: npm ${command}`); process.env.HEAVY_DEBUG && console.log(`🪲 npm ${command} cwd:`, cwd); process.env.HEAVY_DEBUG && console.time(`npm ${command} done in`); - const { stdout: out, stderr: err } = await execAsync(`${nodeBinary} ${npmPath} ${command}`, { + + // On Windows, execute npm.cmd directly; on Unix, use node + npm + let execCommand: string; + if (process.platform === 'win32') { + // Quote path if it contains spaces + const quotedNpmPath = npmPath.includes(' ') ? `"${npmPath}"` : npmPath; + execCommand = `${quotedNpmPath} ${command}`; + } else { + // Quote paths that contain spaces (for Unix systems) + const quotedNodeBinary = nodeBinary.includes(' ') ? `"${nodeBinary}"` : nodeBinary; + const quotedNpmPath = npmPath.includes(' ') ? `"${npmPath}"` : npmPath; + execCommand = `${quotedNodeBinary} ${quotedNpmPath} ${command}`; + } + + const execOptions: any = { cwd, env, - }); + }; + + // On Windows, use shell to execute .cmd files + if (process.platform === 'win32') { + execOptions.shell = true; + } + + const { stdout: out, stderr: err } = await execAsync(execCommand, execOptions); process.env.HEAVY_DEBUG && console.timeEnd(`npm ${command} done in`); // process.env.HEAVY_DEBUG && console.log(`🪲 npm ${command} output:`, out); @@ -357,8 +384,8 @@ class CodeInjector implements ICodeInjector { await fsExtra.copy(src, to, { recursive: true, dereference: true, - // exclue if node_modules comes after /custom/ in path - filter: (src) => !src.includes('/custom/node_modules'), + // exclude if node_modules comes after /custom/ in path + filter: (src) => !src.includes(path.join('custom', 'node_modules')), }); } @@ -668,7 +695,7 @@ class CodeInjector implements ICodeInjector { 'change', async (file) => { process.env.HEAVY_DEBUG && console.log(`🐛 File ${file} changed (SPA), preparing sources...`); - await this.updatePartials({ filesUpdated: [file.replace(spaPath + '/', '')] }); + await this.updatePartials({ filesUpdated: [file.replace(spaPath + path.sep, '')] }); } ) watcher.on('fallback', notifyWatcherIssue); @@ -725,7 +752,7 @@ class CodeInjector implements ICodeInjector { 'change', async (fileOrDir) => { // copy one file - const relativeFilename = fileOrDir.replace(customComponentsDir + '/', ''); + const relativeFilename = fileOrDir.replace(customComponentsDir + path.sep, ''); if (process.env.HEAVY_DEBUG) { console.log(`🔎 fileOrDir ${fileOrDir} changed`); console.log(`🔎 relativeFilename ${relativeFilename}`); @@ -867,18 +894,21 @@ class CodeInjector implements ICodeInjector { const command = 'run dev'; console.log(`⚙️ spawn: npm ${command}...`); - const nodeBinary = process.execPath; - const npmPath = path.join(path.dirname(nodeBinary), 'npm'); const env = { VITE_ADMINFORTH_PUBLIC_PATH: this.adminforth.config.baseUrl, FORCE_COLOR: '1', ...process.env, }; - const devServer = spawn(`${nodeBinary}`, [`${npmPath}`, ...command.split(' ')], { - cwd, - env, - }); + const nodeBinary = process.execPath; + const npmPath = path.join(path.dirname(nodeBinary), 'npm'); + + let devServer; + if (process.platform === 'win32') { + devServer = spawn('npm', command.split(' '), { cwd, env, shell: true }); + } else { + devServer = spawn(`${nodeBinary}`, [`${npmPath}`, ...command.split(' ')], { cwd, env }); + } devServer.stdout.on('data', (data) => { if (data.includes('➜')) { // TODO: maybe better use our string "App port: 5174. HMR port: 5274", it is more reliable because vue might change their output diff --git a/adminforth/modules/configValidator.ts b/adminforth/modules/configValidator.ts index 684e0103..ecef282b 100644 --- a/adminforth/modules/configValidator.ts +++ b/adminforth/modules/configValidator.ts @@ -115,10 +115,11 @@ export default class ConfigValidator implements IConfigValidator { const loginPageInjections: AdminForthConfigCustomization['loginPageInjections'] = { underInputs: [], + panelHeader: [], }; if (this.inputConfig.customization?.loginPageInjections) { - const ALLOWED_LOGIN_INJECTIONS = ['underInputs'] + const ALLOWED_LOGIN_INJECTIONS = ['underInputs', 'panelHeader'] Object.keys(this.inputConfig.customization.loginPageInjections).forEach((injection) => { if (ALLOWED_LOGIN_INJECTIONS.includes(injection)) { loginPageInjections[injection] = this.validateAndListifyInjectionNew(this.inputConfig.customization.loginPageInjections, injection, errors); @@ -942,8 +943,16 @@ export default class ConfigValidator implements IConfigValidator { throw new Error(`Resource with id "${newConfig.auth.usersResourceId}" not found. ${similar ? `Did you mean "${similar}"?` : ''}`); } - if (!newConfig.auth.beforeLoginConfirmation) { - newConfig.auth.beforeLoginConfirmation = []; + // normalize beforeLoginConfirmation hooks + const blc = this.inputConfig.auth.beforeLoginConfirmation; + if (!Array.isArray(blc)) { + if (blc) { + newConfig.auth.beforeLoginConfirmation = [blc]; + } else { + newConfig.auth.beforeLoginConfirmation = []; + } + } else { + newConfig.auth.beforeLoginConfirmation = blc; } } diff --git a/adminforth/modules/restApi.ts b/adminforth/modules/restApi.ts index 499ee606..c1c965cf 100644 --- a/adminforth/modules/restApi.ts +++ b/adminforth/modules/restApi.ts @@ -84,22 +84,21 @@ export default class AdminForthRestAPI implements IAdminForthRestAPI { async processLoginCallbacks(adminUser: AdminUser, toReturn: { redirectTo?: string, allowedLogin:boolean, error?: string }, response: any, extra: HttpExtra) { const beforeLoginConfirmation = this.adminforth.config.auth.beforeLoginConfirmation as (BeforeLoginConfirmationFunction[] | undefined); - if (beforeLoginConfirmation?.length){ - for (const hook of beforeLoginConfirmation) { - const resp = await hook({ - adminUser, - response, - adminforth: this.adminforth, - extra, - }); - - if (resp?.body?.redirectTo || resp?.error) { - // delete all items from toReturn and add these: - toReturn.redirectTo = resp?.body?.redirectTo; - toReturn.allowedLogin = resp?.body?.allowedLogin; - toReturn.error = resp?.error; - break; - } + + for (const hook of listify(beforeLoginConfirmation)) { + const resp = await hook({ + adminUser, + response, + adminforth: this.adminforth, + extra, + }); + + if (resp?.body?.redirectTo || resp?.error) { + // delete all items from toReturn and add these: + toReturn.redirectTo = resp?.body?.redirectTo; + toReturn.allowedLogin = resp?.body?.allowedLogin; + toReturn.error = resp?.error; + break; } } } diff --git a/adminforth/modules/utils.ts b/adminforth/modules/utils.ts index 4ade5ba5..bbe657b2 100644 --- a/adminforth/modules/utils.ts +++ b/adminforth/modules/utils.ts @@ -173,8 +173,11 @@ export function guessLabelFromName(name) { export const currentFileDir = (metaUrl) => { const __filename = fileURLToPath(metaUrl); let __dirname = path.dirname(__filename); - if (__dirname.endsWith('/dist/modules')) { - // in prod build we are in dist also, so make another back jump to go true sorces + + // Check for dist/modules in both Unix and Windows path formats + const normalizedDir = __dirname.replace(/\\/g, '/'); + if (normalizedDir.endsWith('/dist/modules')) { + // in prod build we are in dist also, so make another back jump to go to sources __dirname = path.join(__dirname, '..'); } return __dirname; diff --git a/adminforth/package.json b/adminforth/package.json index 92dae202..6f39e22e 100644 --- a/adminforth/package.json +++ b/adminforth/package.json @@ -20,7 +20,7 @@ "rollout-doc": "cd documentation && npm run build && npm run deploy", "docs": "typedoc", "--comment_postinstall": "postinstall executed after package installed in other project package and when we do npm ci in the package", - "postinstall": "if test -d ./dist/spa/; then cd ./dist/spa/ && npm ci && echo 'installed spa dependencies'; fi" + "postinstall": "node -e \"const fs=require('fs');const path=require('path');const spaPath=path.join(__dirname,'dist','spa');if(fs.existsSync(spaPath)){process.chdir(spaPath);require('child_process').execSync('npm ci',{stdio:'inherit'});console.log('installed spa dependencies');}\"" }, "release": { "plugins": [ diff --git a/adminforth/spa/package.json b/adminforth/spa/package.json index 44daa28f..41777ae9 100644 --- a/adminforth/spa/package.json +++ b/adminforth/spa/package.json @@ -10,7 +10,7 @@ "build-only": "vite build", "type-check": "vue-tsc --build --force", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", - "i18n:extract": "echo '{}' > i18n-empty.json && vue-i18n-extract report --vueFiles './src/**/*.?(js|vue)' --output ./i18n-messages.json --languageFiles 'i18n-empty.json' --add" + "i18n:extract": "echo {} > i18n-empty.json && vue-i18n-extract report --vueFiles \"./src/**/*.{js,vue}\" --output ./i18n-messages.json --languageFiles \"i18n-empty.json\" --add" }, "dependencies": { "@iconify-prerendered/vue-flag": "^0.28.1748584105", diff --git a/adminforth/spa/src/afcl/Button.vue b/adminforth/spa/src/afcl/Button.vue index 0d1a1178..ee7557eb 100644 --- a/adminforth/spa/src/afcl/Button.vue +++ b/adminforth/spa/src/afcl/Button.vue @@ -2,7 +2,7 @@