diff --git a/404.html b/404.html index 206ff2138..9a76b2b47 100644 --- a/404.html +++ b/404.html @@ -15,8 +15,8 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

diff --git a/assets/images/image-16-6cbe8efea058e3cf92a2e2a9e776c6fa.png b/assets/images/image-16-6cbe8efea058e3cf92a2e2a9e776c6fa.png new file mode 100644 index 000000000..420d68e44 Binary files /dev/null and b/assets/images/image-16-6cbe8efea058e3cf92a2e2a9e776c6fa.png differ diff --git a/assets/js/11b43341.1ffc47f0.js b/assets/js/11b43341.1ffc47f0.js new file mode 100644 index 000000000..9cee4fd89 --- /dev/null +++ b/assets/js/11b43341.1ffc47f0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[2256],{7674:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"Getting Started","href":"/docs/tutorial/gettingStarted","docId":"tutorial/gettingStarted","unlisted":false},{"type":"link","label":"Hello world app","href":"/docs/tutorial/helloWorld","docId":"tutorial/helloWorld","unlisted":false},{"type":"link","label":"Glossary","href":"/docs/tutorial/glossary","docId":"tutorial/glossary","unlisted":false},{"type":"category","label":"Customization","collapsible":false,"collapsed":false,"items":[{"type":"link","label":"Branding and Theming","href":"/docs/tutorial/Customization/branding","docId":"tutorial/Customization/branding","unlisted":false},{"type":"link","label":"Custom record field rendering","href":"/docs/tutorial/Customization/customFieldRendering","docId":"tutorial/Customization/customFieldRendering","unlisted":false},{"type":"link","label":"Virtual columns","href":"/docs/tutorial/Customization/virtualColumns","docId":"tutorial/Customization/virtualColumns","unlisted":false},{"type":"link","label":"Hooks","href":"/docs/tutorial/Customization/hooks","docId":"tutorial/Customization/hooks","unlisted":false},{"type":"link","label":"Limiting actions access","href":"/docs/tutorial/Customization/limitingAccess","docId":"tutorial/Customization/limitingAccess","unlisted":false},{"type":"link","label":"Custom pages","href":"/docs/tutorial/Customization/customPages","docId":"tutorial/Customization/customPages","unlisted":false},{"type":"link","label":"Alerts and confirmations","href":"/docs/tutorial/Customization/alert","docId":"tutorial/Customization/alert","unlisted":false},{"type":"link","label":"Page Injections","href":"/docs/tutorial/Customization/pageInjections","docId":"tutorial/Customization/pageInjections","unlisted":false},{"type":"link","label":"Custom bulk actions","href":"/docs/tutorial/Customization/bulkActions","docId":"tutorial/Customization/bulkActions","unlisted":false},{"type":"link","label":"Menu Configuration","href":"/docs/tutorial/Customization/menuConfiguration","docId":"tutorial/Customization/menuConfiguration","unlisted":false},{"type":"link","label":"Data API","href":"/docs/tutorial/Customization/dataApi","docId":"tutorial/Customization/dataApi","unlisted":false},{"type":"link","label":"Security","href":"/docs/tutorial/Customization/security","docId":"tutorial/Customization/security","unlisted":false},{"type":"link","label":"Standard pages tuning","href":"/docs/tutorial/Customization/standardPagesTuning","docId":"tutorial/Customization/standardPagesTuning","unlisted":false},{"type":"link","label":"AdminForth Components Library","href":"/docs/tutorial/Customization/afcl","docId":"tutorial/Customization/afcl","unlisted":false}]},{"type":"link","label":"Deploy in Docker","href":"/docs/tutorial/deploy","docId":"tutorial/deploy","unlisted":false},{"type":"category","label":"Plugins","collapsible":false,"collapsed":false,"items":[{"type":"link","label":"AuditLog","href":"/docs/tutorial/Plugins/AuditLog","docId":"tutorial/Plugins/AuditLog","unlisted":false},{"type":"link","label":"Two-Factor Authentication Plugin","href":"/docs/tutorial/Plugins/TwoFactorsAuth","docId":"tutorial/Plugins/TwoFactorsAuth","unlisted":false},{"type":"link","label":"ForeignInlineList","href":"/docs/tutorial/Plugins/ForeignInlineList","docId":"tutorial/Plugins/ForeignInlineList","unlisted":false},{"type":"link","label":"Rich editor","href":"/docs/tutorial/Plugins/RichEditor","docId":"tutorial/Plugins/RichEditor","unlisted":false},{"type":"link","label":"Upload","href":"/docs/tutorial/Plugins/upload","docId":"tutorial/Plugins/upload","unlisted":false},{"type":"link","label":"Chat-GPT","href":"/docs/tutorial/Plugins/chat-gpt","docId":"tutorial/Plugins/chat-gpt","unlisted":false},{"type":"link","label":"Email password reset","href":"/docs/tutorial/Plugins/email-password-reset","docId":"tutorial/Plugins/email-password-reset","unlisted":false},{"type":"link","label":"Import-Export","href":"/docs/tutorial/Plugins/import-export","docId":"tutorial/Plugins/import-export","unlisted":false}]},{"type":"category","label":"Advanced","collapsible":false,"collapsed":false,"items":[{"type":"link","label":"Plugin development guide","href":"/docs/tutorial/Advanced/plugin-development","docId":"tutorial/Advanced/plugin-development","unlisted":false}]}],"apiSidebar":[{"type":"category","label":"API","items":[{"type":"category","label":"types","items":[{"type":"category","label":"Back","items":[{"type":"category","label":"Enumerations","items":[{"type":"link","label":"AdminForthMenuTypes","href":"/docs/api/types/Back/enumerations/AdminForthMenuTypes","docId":"api/types/Back/enumerations/AdminForthMenuTypes","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Classes","items":[{"type":"link","label":"Filters","href":"/docs/api/types/Back/classes/Filters","docId":"api/types/Back/classes/Filters","unlisted":false},{"type":"link","label":"Sorts","href":"/docs/api/types/Back/classes/Sorts","docId":"api/types/Back/classes/Sorts","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Interfaces","items":[{"type":"link","label":"AdminForthBulkAction","href":"/docs/api/types/Back/interfaces/AdminForthBulkAction","docId":"api/types/Back/interfaces/AdminForthBulkAction","unlisted":false},{"type":"link","label":"AdminForthConfig","href":"/docs/api/types/Back/interfaces/AdminForthConfig","docId":"api/types/Back/interfaces/AdminForthConfig","unlisted":false},{"type":"link","label":"AdminForthForeignResource","href":"/docs/api/types/Back/interfaces/AdminForthForeignResource","docId":"api/types/Back/interfaces/AdminForthForeignResource","unlisted":false},{"type":"link","label":"AdminForthResource","href":"/docs/api/types/Back/interfaces/AdminForthResource","docId":"api/types/Back/interfaces/AdminForthResource","unlisted":false},{"type":"link","label":"AdminForthResourceColumn","href":"/docs/api/types/Back/interfaces/AdminForthResourceColumn","docId":"api/types/Back/interfaces/AdminForthResourceColumn","unlisted":false},{"type":"link","label":"IAdminForth","href":"/docs/api/types/Back/interfaces/IAdminForth","docId":"api/types/Back/interfaces/IAdminForth","unlisted":false},{"type":"link","label":"IAdminForthAuth","href":"/docs/api/types/Back/interfaces/IAdminForthAuth","docId":"api/types/Back/interfaces/IAdminForthAuth","unlisted":false},{"type":"link","label":"IAdminForthDataSourceConnector","href":"/docs/api/types/Back/interfaces/IAdminForthDataSourceConnector","docId":"api/types/Back/interfaces/IAdminForthDataSourceConnector","unlisted":false},{"type":"link","label":"IAdminForthDataSourceConnectorBase","href":"/docs/api/types/Back/interfaces/IAdminForthDataSourceConnectorBase","docId":"api/types/Back/interfaces/IAdminForthDataSourceConnectorBase","unlisted":false},{"type":"link","label":"IAdminForthDataSourceConnectorConstructor","href":"/docs/api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor","docId":"api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor","unlisted":false},{"type":"link","label":"IAdminForthFilter","href":"/docs/api/types/Back/interfaces/IAdminForthFilter","docId":"api/types/Back/interfaces/IAdminForthFilter","unlisted":false},{"type":"link","label":"IAdminForthHttpResponse","href":"/docs/api/types/Back/interfaces/IAdminForthHttpResponse","docId":"api/types/Back/interfaces/IAdminForthHttpResponse","unlisted":false},{"type":"link","label":"IAdminForthPlugin","href":"/docs/api/types/Back/interfaces/IAdminForthPlugin","docId":"api/types/Back/interfaces/IAdminForthPlugin","unlisted":false},{"type":"link","label":"IAdminForthSort","href":"/docs/api/types/Back/interfaces/IAdminForthSort","docId":"api/types/Back/interfaces/IAdminForthSort","unlisted":false},{"type":"link","label":"ICodeInjector","href":"/docs/api/types/Back/interfaces/ICodeInjector","docId":"api/types/Back/interfaces/ICodeInjector","unlisted":false},{"type":"link","label":"IConfigValidator","href":"/docs/api/types/Back/interfaces/IConfigValidator","docId":"api/types/Back/interfaces/IConfigValidator","unlisted":false},{"type":"link","label":"IExpressHttpServer","href":"/docs/api/types/Back/interfaces/IExpressHttpServer","docId":"api/types/Back/interfaces/IExpressHttpServer","unlisted":false},{"type":"link","label":"IHttpServer","href":"/docs/api/types/Back/interfaces/IHttpServer","docId":"api/types/Back/interfaces/IHttpServer","unlisted":false},{"type":"link","label":"IOperationalResource","href":"/docs/api/types/Back/interfaces/IOperationalResource","docId":"api/types/Back/interfaces/IOperationalResource","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Type Aliases","items":[{"type":"link","label":"AdminForthConfigMenuItem","href":"/docs/api/types/Back/type-aliases/AdminForthConfigMenuItem","docId":"api/types/Back/type-aliases/AdminForthConfigMenuItem","unlisted":false},{"type":"link","label":"AdminForthDataSource","href":"/docs/api/types/Back/type-aliases/AdminForthDataSource","docId":"api/types/Back/type-aliases/AdminForthDataSource","unlisted":false},{"type":"link","label":"AfterDataSourceResponseFunction","href":"/docs/api/types/Back/type-aliases/AfterDataSourceResponseFunction","docId":"api/types/Back/type-aliases/AfterDataSourceResponseFunction","unlisted":false},{"type":"link","label":"AfterSaveFunction","href":"/docs/api/types/Back/type-aliases/AfterSaveFunction","docId":"api/types/Back/type-aliases/AfterSaveFunction","unlisted":false},{"type":"link","label":"AllowedActionValue","href":"/docs/api/types/Back/type-aliases/AllowedActionValue","docId":"api/types/Back/type-aliases/AllowedActionValue","unlisted":false},{"type":"link","label":"AllowedActions","href":"/docs/api/types/Back/type-aliases/AllowedActions","docId":"api/types/Back/type-aliases/AllowedActions","unlisted":false},{"type":"link","label":"BeforeDataSourceRequestFunction","href":"/docs/api/types/Back/type-aliases/BeforeDataSourceRequestFunction","docId":"api/types/Back/type-aliases/BeforeDataSourceRequestFunction","unlisted":false},{"type":"link","label":"BeforeLoginConfirmationFunction","href":"/docs/api/types/Back/type-aliases/BeforeLoginConfirmationFunction","docId":"api/types/Back/type-aliases/BeforeLoginConfirmationFunction","unlisted":false},{"type":"link","label":"BeforeSaveFunction","href":"/docs/api/types/Back/type-aliases/BeforeSaveFunction","docId":"api/types/Back/type-aliases/BeforeSaveFunction","unlisted":false},{"type":"link","label":"FDataFilter","href":"/docs/api/types/Back/type-aliases/FDataFilter","docId":"api/types/Back/type-aliases/FDataFilter","unlisted":false},{"type":"link","label":"FDataSort","href":"/docs/api/types/Back/type-aliases/FDataSort","docId":"api/types/Back/type-aliases/FDataSort","unlisted":false},{"type":"link","label":"ResourceOptions","href":"/docs/api/types/Back/type-aliases/ResourceOptions","docId":"api/types/Back/type-aliases/ResourceOptions","unlisted":false}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/types/Back/"},{"type":"category","label":"Common","items":[{"type":"category","label":"Enumerations","items":[{"type":"link","label":"ActionCheckSource","href":"/docs/api/types/Common/enumerations/ActionCheckSource","docId":"api/types/Common/enumerations/ActionCheckSource","unlisted":false},{"type":"link","label":"AdminForthDataTypes","href":"/docs/api/types/Common/enumerations/AdminForthDataTypes","docId":"api/types/Common/enumerations/AdminForthDataTypes","unlisted":false},{"type":"link","label":"AdminForthFilterOperators","href":"/docs/api/types/Common/enumerations/AdminForthFilterOperators","docId":"api/types/Common/enumerations/AdminForthFilterOperators","unlisted":false},{"type":"link","label":"AdminForthResourcePages","href":"/docs/api/types/Common/enumerations/AdminForthResourcePages","docId":"api/types/Common/enumerations/AdminForthResourcePages","unlisted":false},{"type":"link","label":"AdminForthSortDirections","href":"/docs/api/types/Common/enumerations/AdminForthSortDirections","docId":"api/types/Common/enumerations/AdminForthSortDirections","unlisted":false},{"type":"link","label":"AllowedActionsEnum","href":"/docs/api/types/Common/enumerations/AllowedActionsEnum","docId":"api/types/Common/enumerations/AllowedActionsEnum","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Interfaces","items":[{"type":"link","label":"AdminForthBulkActionCommon","href":"/docs/api/types/Common/interfaces/AdminForthBulkActionCommon","docId":"api/types/Common/interfaces/AdminForthBulkActionCommon","unlisted":false},{"type":"link","label":"AdminForthColumnEnumItem","href":"/docs/api/types/Common/interfaces/AdminForthColumnEnumItem","docId":"api/types/Common/interfaces/AdminForthColumnEnumItem","unlisted":false},{"type":"link","label":"AdminForthComponentDeclarationFull","href":"/docs/api/types/Common/interfaces/AdminForthComponentDeclarationFull","docId":"api/types/Common/interfaces/AdminForthComponentDeclarationFull","unlisted":false},{"type":"link","label":"AdminForthFieldComponents","href":"/docs/api/types/Common/interfaces/AdminForthFieldComponents","docId":"api/types/Common/interfaces/AdminForthFieldComponents","unlisted":false},{"type":"link","label":"AdminForthForeignResourceCommon","href":"/docs/api/types/Common/interfaces/AdminForthForeignResourceCommon","docId":"api/types/Common/interfaces/AdminForthForeignResourceCommon","unlisted":false},{"type":"link","label":"AdminForthResourceCommon","href":"/docs/api/types/Common/interfaces/AdminForthResourceCommon","docId":"api/types/Common/interfaces/AdminForthResourceCommon","unlisted":false},{"type":"link","label":"AdminUser","href":"/docs/api/types/Common/interfaces/AdminUser","docId":"api/types/Common/interfaces/AdminUser","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Type Aliases","items":[{"type":"link","label":"AdminForthComponentDeclaration","href":"/docs/api/types/Common/type-aliases/AdminForthComponentDeclaration","docId":"api/types/Common/type-aliases/AdminForthComponentDeclaration","unlisted":false},{"type":"link","label":"AdminForthResourceColumnCommon","href":"/docs/api/types/Common/type-aliases/AdminForthResourceColumnCommon","docId":"api/types/Common/type-aliases/AdminForthResourceColumnCommon","unlisted":false},{"type":"link","label":"AllowedActionsResolved","href":"/docs/api/types/Common/type-aliases/AllowedActionsResolved","docId":"api/types/Common/type-aliases/AllowedActionsResolved","unlisted":false},{"type":"link","label":"ValidationObject","href":"/docs/api/types/Common/type-aliases/ValidationObject","docId":"api/types/Common/type-aliases/ValidationObject","unlisted":false}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/types/Common/"},{"type":"category","label":"FrontendAPI","items":[{"type":"category","label":"Enumerations","items":[{"type":"link","label":"AlertVariant","href":"/docs/api/types/FrontendAPI/enumerations/AlertVariant","docId":"api/types/FrontendAPI/enumerations/AlertVariant","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Interfaces","items":[{"type":"link","label":"FrontendAPIInterface","href":"/docs/api/types/FrontendAPI/interfaces/FrontendAPIInterface","docId":"api/types/FrontendAPI/interfaces/FrontendAPIInterface","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Type Aliases","items":[{"type":"link","label":"AlertParams","href":"/docs/api/types/FrontendAPI/type-aliases/AlertParams","docId":"api/types/FrontendAPI/type-aliases/AlertParams","unlisted":false},{"type":"link","label":"ConfirmParams","href":"/docs/api/types/FrontendAPI/type-aliases/ConfirmParams","docId":"api/types/FrontendAPI/type-aliases/ConfirmParams","unlisted":false}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/types/FrontendAPI/"}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/"}]},"docs":{"api/index":{"id":"api/index","title":"TypeDoc API","description":"Modules","sidebar":"apiSidebar"},"api/types/Back/classes/Filters":{"id":"api/types/Back/classes/Filters","title":"Filters","description":"Constructors","sidebar":"apiSidebar"},"api/types/Back/classes/Sorts":{"id":"api/types/Back/classes/Sorts","title":"Sorts","description":"Constructors","sidebar":"apiSidebar"},"api/types/Back/enumerations/AdminForthMenuTypes":{"id":"api/types/Back/enumerations/AdminForthMenuTypes","title":"AdminForthMenuTypes","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Back/index":{"id":"api/types/Back/index","title":"types/Back","description":"Index","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthBulkAction":{"id":"api/types/Back/interfaces/AdminForthBulkAction","title":"AdminForthBulkAction","description":"Extends","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthConfig":{"id":"api/types/Back/interfaces/AdminForthConfig","title":"AdminForthConfig","description":"Main configuration object for AdminForth","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthForeignResource":{"id":"api/types/Back/interfaces/AdminForthForeignResource","title":"AdminForthForeignResource","description":"Extends","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthResource":{"id":"api/types/Back/interfaces/AdminForthResource","title":"AdminForthResource","description":"Resource describes one table or collection in database.","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthResourceColumn":{"id":"api/types/Back/interfaces/AdminForthResourceColumn","title":"AdminForthResourceColumn","description":"Column describes one field in the table or collection in database.","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForth":{"id":"api/types/Back/interfaces/IAdminForth","title":"IAdminForth","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthAuth":{"id":"api/types/Back/interfaces/IAdminForthAuth","title":"IAdminForthAuth","description":"Methods","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthDataSourceConnector":{"id":"api/types/Back/interfaces/IAdminForthDataSourceConnector","title":"IAdminForthDataSourceConnector","description":"Extended by","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthDataSourceConnectorBase":{"id":"api/types/Back/interfaces/IAdminForthDataSourceConnectorBase","title":"IAdminForthDataSourceConnectorBase","description":"Interface that exposes methods to interact with AdminForth in standard way","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor":{"id":"api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor","title":"IAdminForthDataSourceConnectorConstructor","description":"Constructors","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthFilter":{"id":"api/types/Back/interfaces/IAdminForthFilter","title":"IAdminForthFilter","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthHttpResponse":{"id":"api/types/Back/interfaces/IAdminForthHttpResponse","title":"IAdminForthHttpResponse","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthPlugin":{"id":"api/types/Back/interfaces/IAdminForthPlugin","title":"IAdminForthPlugin","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthSort":{"id":"api/types/Back/interfaces/IAdminForthSort","title":"IAdminForthSort","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/ICodeInjector":{"id":"api/types/Back/interfaces/ICodeInjector","title":"ICodeInjector","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IConfigValidator":{"id":"api/types/Back/interfaces/IConfigValidator","title":"IConfigValidator","description":"Methods","sidebar":"apiSidebar"},"api/types/Back/interfaces/IExpressHttpServer":{"id":"api/types/Back/interfaces/IExpressHttpServer","title":"IExpressHttpServer","description":"Implement this interface to create custom HTTP server adapter for AdminForth.","sidebar":"apiSidebar"},"api/types/Back/interfaces/IHttpServer":{"id":"api/types/Back/interfaces/IHttpServer","title":"IHttpServer","description":"Implement this interface to create custom HTTP server adapter for AdminForth.","sidebar":"apiSidebar"},"api/types/Back/interfaces/IOperationalResource":{"id":"api/types/Back/interfaces/IOperationalResource","title":"IOperationalResource","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AdminForthConfigMenuItem":{"id":"api/types/Back/type-aliases/AdminForthConfigMenuItem","title":"AdminForthConfigMenuItem","description":"AdminForthConfigMenuItem: object","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AdminForthDataSource":{"id":"api/types/Back/type-aliases/AdminForthDataSource","title":"AdminForthDataSource","description":"AdminForthDataSource: object","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AfterDataSourceResponseFunction":{"id":"api/types/Back/type-aliases/AfterDataSourceResponseFunction","title":"AfterDataSourceResponseFunction()","description":"AfterDataSourceResponseFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AfterSaveFunction":{"id":"api/types/Back/type-aliases/AfterSaveFunction","title":"AfterSaveFunction()","description":"AfterSaveFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AllowedActions":{"id":"api/types/Back/type-aliases/AllowedActions","title":"AllowedActions","description":"AllowedActions AllowedActionValue } & object","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AllowedActionValue":{"id":"api/types/Back/type-aliases/AllowedActionValue","title":"AllowedActionValue","description":"AllowedActionValue: boolean \\\\| ({adminUser, resource, meta, source}) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/BeforeDataSourceRequestFunction":{"id":"api/types/Back/type-aliases/BeforeDataSourceRequestFunction","title":"BeforeDataSourceRequestFunction()","description":"BeforeDataSourceRequestFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/BeforeLoginConfirmationFunction":{"id":"api/types/Back/type-aliases/BeforeLoginConfirmationFunction","title":"BeforeLoginConfirmationFunction()","description":"BeforeLoginConfirmationFunction: (params?) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/BeforeSaveFunction":{"id":"api/types/Back/type-aliases/BeforeSaveFunction","title":"BeforeSaveFunction()","description":"BeforeSaveFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/FDataFilter":{"id":"api/types/Back/type-aliases/FDataFilter","title":"FDataFilter()","description":"FDataFilter: (field, value) => IAdminForthFilter","sidebar":"apiSidebar"},"api/types/Back/type-aliases/FDataSort":{"id":"api/types/Back/type-aliases/FDataSort","title":"FDataSort()","description":"FDataSort: (field, direction) => IAdminForthSort","sidebar":"apiSidebar"},"api/types/Back/type-aliases/ResourceOptions":{"id":"api/types/Back/type-aliases/ResourceOptions","title":"ResourceOptions","description":"ResourceOptions: Omit \\\\ & object","sidebar":"apiSidebar"},"api/types/Common/enumerations/ActionCheckSource":{"id":"api/types/Common/enumerations/ActionCheckSource","title":"ActionCheckSource","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthDataTypes":{"id":"api/types/Common/enumerations/AdminForthDataTypes","title":"AdminForthDataTypes","description":"Types that are common for both frontend side (SPA) and backend side (server).","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthFilterOperators":{"id":"api/types/Common/enumerations/AdminForthFilterOperators","title":"AdminForthFilterOperators","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthResourcePages":{"id":"api/types/Common/enumerations/AdminForthResourcePages","title":"AdminForthResourcePages","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthSortDirections":{"id":"api/types/Common/enumerations/AdminForthSortDirections","title":"AdminForthSortDirections","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AllowedActionsEnum":{"id":"api/types/Common/enumerations/AllowedActionsEnum","title":"AllowedActionsEnum","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/index":{"id":"api/types/Common/index","title":"types/Common","description":"Index","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthBulkActionCommon":{"id":"api/types/Common/interfaces/AdminForthBulkActionCommon","title":"AdminForthBulkActionCommon","description":"Extended by","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthColumnEnumItem":{"id":"api/types/Common/interfaces/AdminForthColumnEnumItem","title":"AdminForthColumnEnumItem","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthComponentDeclarationFull":{"id":"api/types/Common/interfaces/AdminForthComponentDeclarationFull","title":"AdminForthComponentDeclarationFull","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthFieldComponents":{"id":"api/types/Common/interfaces/AdminForthFieldComponents","title":"AdminForthFieldComponents","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthForeignResourceCommon":{"id":"api/types/Common/interfaces/AdminForthForeignResourceCommon","title":"AdminForthForeignResourceCommon","description":"Extended by","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthResourceCommon":{"id":"api/types/Common/interfaces/AdminForthResourceCommon","title":"AdminForthResourceCommon","description":"Resource describes one table or collection in database.","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminUser":{"id":"api/types/Common/interfaces/AdminUser","title":"AdminUser","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/type-aliases/AdminForthComponentDeclaration":{"id":"api/types/Common/type-aliases/AdminForthComponentDeclaration","title":"AdminForthComponentDeclaration","description":"AdminForthComponentDeclaration: AdminForthComponentDeclarationFull \\\\| string","sidebar":"apiSidebar"},"api/types/Common/type-aliases/AdminForthResourceColumnCommon":{"id":"api/types/Common/type-aliases/AdminForthResourceColumnCommon","title":"AdminForthResourceColumnCommon","description":"AdminForthResourceColumnCommon: object","sidebar":"apiSidebar"},"api/types/Common/type-aliases/AllowedActionsResolved":{"id":"api/types/Common/type-aliases/AllowedActionsResolved","title":"AllowedActionsResolved","description":"AllowedActionsResolved boolean }","sidebar":"apiSidebar"},"api/types/Common/type-aliases/ValidationObject":{"id":"api/types/Common/type-aliases/ValidationObject","title":"ValidationObject","description":"ValidationObject: object","sidebar":"apiSidebar"},"api/types/FrontendAPI/enumerations/AlertVariant":{"id":"api/types/FrontendAPI/enumerations/AlertVariant","title":"AlertVariant","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/FrontendAPI/index":{"id":"api/types/FrontendAPI/index","title":"types/FrontendAPI","description":"Index","sidebar":"apiSidebar"},"api/types/FrontendAPI/interfaces/FrontendAPIInterface":{"id":"api/types/FrontendAPI/interfaces/FrontendAPIInterface","title":"FrontendAPIInterface","description":"Properties","sidebar":"apiSidebar"},"api/types/FrontendAPI/type-aliases/AlertParams":{"id":"api/types/FrontendAPI/type-aliases/AlertParams","title":"AlertParams","description":"AlertParams: object","sidebar":"apiSidebar"},"api/types/FrontendAPI/type-aliases/ConfirmParams":{"id":"api/types/FrontendAPI/type-aliases/ConfirmParams","title":"ConfirmParams","description":"ConfirmParams: object","sidebar":"apiSidebar"},"tutorial/Advanced/plugin-development":{"id":"tutorial/Advanced/plugin-development","title":"Plugin development guide","description":"Creating a plugin is a powerful way to extend AdminForth functionality.","sidebar":"tutorialSidebar"},"tutorial/Customization/afcl":{"id":"tutorial/Customization/afcl","title":"AdminForth Components Library","description":"ACL is a new set of components which you can use as build blocks.","sidebar":"tutorialSidebar"},"tutorial/Customization/alert":{"id":"tutorial/Customization/alert","title":"Alerts and confirmations","description":"When you are writing custom components or pages you might need to show alerts or confirmations to the user.","sidebar":"tutorialSidebar"},"tutorial/Customization/branding":{"id":"tutorial/Customization/branding","title":"Branding and Theming","description":"The first things you would probably like to change are the logo, favicon and the name of the application.","sidebar":"tutorialSidebar"},"tutorial/Customization/bulkActions":{"id":"tutorial/Customization/bulkActions","title":"Custom bulk actions","description":"You might need to give admin users a feature to perform same action on multiple records at once.","sidebar":"tutorialSidebar"},"tutorial/Customization/customFieldRendering":{"id":"tutorial/Customization/customFieldRendering","title":"Custom record field rendering","description":"Customizing how AdminForth renders the cells with record values","sidebar":"tutorialSidebar"},"tutorial/Customization/customPages":{"id":"tutorial/Customization/customPages","title":"Custom pages","description":"Most Admin Panels should have some Dashboards or custom pages.","sidebar":"tutorialSidebar"},"tutorial/Customization/dataApi":{"id":"tutorial/Customization/dataApi","title":"Data API","description":"AdminForth Data API is a minimal set of methods to manipulate the data in the database.","sidebar":"tutorialSidebar"},"tutorial/Customization/hooks":{"id":"tutorial/Customization/hooks","title":"Hooks","description":"Hooks are used to:","sidebar":"tutorialSidebar"},"tutorial/Customization/limitingAccess":{"id":"tutorial/Customization/limitingAccess","title":"Limiting actions access","description":"Statically disable some action","sidebar":"tutorialSidebar"},"tutorial/Customization/menuConfiguration":{"id":"tutorial/Customization/menuConfiguration","title":"Menu Configuration","description":"Icons","sidebar":"tutorialSidebar"},"tutorial/Customization/pageInjections":{"id":"tutorial/Customization/pageInjections","title":"Page Injections","description":"In addition to ability to create custom pages and overwrite how fields are rendered, you can also inject custom components in standard AdminForth page.","sidebar":"tutorialSidebar"},"tutorial/Customization/security":{"id":"tutorial/Customization/security","title":"Security","description":"Security and privacy if adminforth users is one of the most important aspects of AdminForth.","sidebar":"tutorialSidebar"},"tutorial/Customization/standardPagesTuning":{"id":"tutorial/Customization/standardPagesTuning","title":"Standard pages tuning","description":"Fields Grouping","sidebar":"tutorialSidebar"},"tutorial/Customization/virtualColumns":{"id":"tutorial/Customization/virtualColumns","title":"Virtual columns","description":"Virtual column for show and list","sidebar":"tutorialSidebar"},"tutorial/deploy":{"id":"tutorial/deploy","title":"Deploy in Docker","description":"In general you can already run your index.ts file which we created in Getting Started","sidebar":"tutorialSidebar"},"tutorial/gettingStarted":{"id":"tutorial/gettingStarted","title":"Getting Started","description":"This Getting Started Page has some explanations and tables with various field types.","sidebar":"tutorialSidebar"},"tutorial/glossary":{"id":"tutorial/glossary","title":"Glossary","description":"dataSource","sidebar":"tutorialSidebar"},"tutorial/helloWorld":{"id":"tutorial/helloWorld","title":"Hello world app","description":"No water. Pure code to get started ASAP.","sidebar":"tutorialSidebar"},"tutorial/Plugins/AuditLog":{"id":"tutorial/Plugins/AuditLog","title":"AuditLog","description":"AuditLog plugin allows to log all changes in the resources done from the admin panel.","sidebar":"tutorialSidebar"},"tutorial/Plugins/chat-gpt":{"id":"tutorial/Plugins/chat-gpt","title":"Chat-GPT","description":"This plugin allows you to auto-complete text and string fields using OpenAI Chat GPT models.","sidebar":"tutorialSidebar"},"tutorial/Plugins/email-password-reset":{"id":"tutorial/Plugins/email-password-reset","title":"Email password reset","description":"Plugin allows to reset password for admin users who forgot their password by sending email with reset link signed with secured JWT token.","sidebar":"tutorialSidebar"},"tutorial/Plugins/ForeignInlineList":{"id":"tutorial/Plugins/ForeignInlineList","title":"ForeignInlineList","description":"Foreign inline list plugin allows to display a list (table) of items from a foreign table in the show view.","sidebar":"tutorialSidebar"},"tutorial/Plugins/import-export":{"id":"tutorial/Plugins/import-export","title":"Import-Export","description":"Import-Export is a plugin that allows you to import and export data from and to a CSV file.","sidebar":"tutorialSidebar"},"tutorial/Plugins/RichEditor":{"id":"tutorial/Plugins/RichEditor","title":"Rich editor","description":"Under the hood this plugin uses Quill. Quill is a free, open source WYSIWYG editor built for the modern web.","sidebar":"tutorialSidebar"},"tutorial/Plugins/TwoFactorsAuth":{"id":"tutorial/Plugins/TwoFactorsAuth","title":"Two-Factor Authentication Plugin","description":"The Two-Factor Authentication Plugin provides an additional layer of security to the application by requiring users to provide a second form of authentication in addition to their password. This plugin supports authenticator apps.","sidebar":"tutorialSidebar"},"tutorial/Plugins/upload":{"id":"tutorial/Plugins/upload","title":"Upload","description":"This plugin allows you to upload files to Amazon S3 bucket.","sidebar":"tutorialSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/11b43341.c9624374.js b/assets/js/11b43341.c9624374.js deleted file mode 100644 index a2e6c7064..000000000 --- a/assets/js/11b43341.c9624374.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[2256],{7674:e=>{e.exports=JSON.parse('{"version":{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"Getting Started","href":"/docs/tutorial/gettingStarted","docId":"tutorial/gettingStarted","unlisted":false},{"type":"link","label":"Hello world app","href":"/docs/tutorial/helloWorld","docId":"tutorial/helloWorld","unlisted":false},{"type":"link","label":"Glossary","href":"/docs/tutorial/glossary","docId":"tutorial/glossary","unlisted":false},{"type":"category","label":"Customization","collapsible":false,"collapsed":false,"items":[{"type":"link","label":"Branding and Theming","href":"/docs/tutorial/Customization/branding","docId":"tutorial/Customization/branding","unlisted":false},{"type":"link","label":"Custom record field rendering","href":"/docs/tutorial/Customization/customFieldRendering","docId":"tutorial/Customization/customFieldRendering","unlisted":false},{"type":"link","label":"Virtual columns","href":"/docs/tutorial/Customization/virtualColumns","docId":"tutorial/Customization/virtualColumns","unlisted":false},{"type":"link","label":"Hooks","href":"/docs/tutorial/Customization/hooks","docId":"tutorial/Customization/hooks","unlisted":false},{"type":"link","label":"Limiting actions access","href":"/docs/tutorial/Customization/limitingAccess","docId":"tutorial/Customization/limitingAccess","unlisted":false},{"type":"link","label":"Custom pages","href":"/docs/tutorial/Customization/customPages","docId":"tutorial/Customization/customPages","unlisted":false},{"type":"link","label":"Alerts and confirmations","href":"/docs/tutorial/Customization/alert","docId":"tutorial/Customization/alert","unlisted":false},{"type":"link","label":"Page Injections","href":"/docs/tutorial/Customization/pageInjections","docId":"tutorial/Customization/pageInjections","unlisted":false},{"type":"link","label":"Custom bulk actions","href":"/docs/tutorial/Customization/bulkActions","docId":"tutorial/Customization/bulkActions","unlisted":false},{"type":"link","label":"Menu Configuration","href":"/docs/tutorial/Customization/menuConfiguration","docId":"tutorial/Customization/menuConfiguration","unlisted":false},{"type":"link","label":"Data API","href":"/docs/tutorial/Customization/dataApi","docId":"tutorial/Customization/dataApi","unlisted":false},{"type":"link","label":"Security","href":"/docs/tutorial/Customization/security","docId":"tutorial/Customization/security","unlisted":false},{"type":"link","label":"Standard pages tuning","href":"/docs/tutorial/Customization/standardPagesTuning","docId":"tutorial/Customization/standardPagesTuning","unlisted":false}]},{"type":"link","label":"Deploy in Docker","href":"/docs/tutorial/deploy","docId":"tutorial/deploy","unlisted":false},{"type":"category","label":"Plugins","collapsible":false,"collapsed":false,"items":[{"type":"link","label":"AuditLog","href":"/docs/tutorial/Plugins/AuditLog","docId":"tutorial/Plugins/AuditLog","unlisted":false},{"type":"link","label":"Two-Factor Authentication Plugin","href":"/docs/tutorial/Plugins/TwoFactorsAuth","docId":"tutorial/Plugins/TwoFactorsAuth","unlisted":false},{"type":"link","label":"ForeignInlineList","href":"/docs/tutorial/Plugins/ForeignInlineList","docId":"tutorial/Plugins/ForeignInlineList","unlisted":false},{"type":"link","label":"Rich editor","href":"/docs/tutorial/Plugins/RichEditor","docId":"tutorial/Plugins/RichEditor","unlisted":false},{"type":"link","label":"Upload","href":"/docs/tutorial/Plugins/upload","docId":"tutorial/Plugins/upload","unlisted":false},{"type":"link","label":"Chat-GPT","href":"/docs/tutorial/Plugins/chat-gpt","docId":"tutorial/Plugins/chat-gpt","unlisted":false},{"type":"link","label":"Email password reset","href":"/docs/tutorial/Plugins/email-password-reset","docId":"tutorial/Plugins/email-password-reset","unlisted":false},{"type":"link","label":"Import-Export","href":"/docs/tutorial/Plugins/import-export","docId":"tutorial/Plugins/import-export","unlisted":false}]},{"type":"category","label":"Advanced","collapsible":false,"collapsed":false,"items":[{"type":"link","label":"Plugin development guide","href":"/docs/tutorial/Advanced/plugin-development","docId":"tutorial/Advanced/plugin-development","unlisted":false}]}],"apiSidebar":[{"type":"category","label":"API","items":[{"type":"category","label":"types","items":[{"type":"category","label":"Back","items":[{"type":"category","label":"Enumerations","items":[{"type":"link","label":"AdminForthMenuTypes","href":"/docs/api/types/Back/enumerations/AdminForthMenuTypes","docId":"api/types/Back/enumerations/AdminForthMenuTypes","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Classes","items":[{"type":"link","label":"Filters","href":"/docs/api/types/Back/classes/Filters","docId":"api/types/Back/classes/Filters","unlisted":false},{"type":"link","label":"Sorts","href":"/docs/api/types/Back/classes/Sorts","docId":"api/types/Back/classes/Sorts","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Interfaces","items":[{"type":"link","label":"AdminForthBulkAction","href":"/docs/api/types/Back/interfaces/AdminForthBulkAction","docId":"api/types/Back/interfaces/AdminForthBulkAction","unlisted":false},{"type":"link","label":"AdminForthConfig","href":"/docs/api/types/Back/interfaces/AdminForthConfig","docId":"api/types/Back/interfaces/AdminForthConfig","unlisted":false},{"type":"link","label":"AdminForthForeignResource","href":"/docs/api/types/Back/interfaces/AdminForthForeignResource","docId":"api/types/Back/interfaces/AdminForthForeignResource","unlisted":false},{"type":"link","label":"AdminForthResource","href":"/docs/api/types/Back/interfaces/AdminForthResource","docId":"api/types/Back/interfaces/AdminForthResource","unlisted":false},{"type":"link","label":"AdminForthResourceColumn","href":"/docs/api/types/Back/interfaces/AdminForthResourceColumn","docId":"api/types/Back/interfaces/AdminForthResourceColumn","unlisted":false},{"type":"link","label":"IAdminForth","href":"/docs/api/types/Back/interfaces/IAdminForth","docId":"api/types/Back/interfaces/IAdminForth","unlisted":false},{"type":"link","label":"IAdminForthAuth","href":"/docs/api/types/Back/interfaces/IAdminForthAuth","docId":"api/types/Back/interfaces/IAdminForthAuth","unlisted":false},{"type":"link","label":"IAdminForthDataSourceConnector","href":"/docs/api/types/Back/interfaces/IAdminForthDataSourceConnector","docId":"api/types/Back/interfaces/IAdminForthDataSourceConnector","unlisted":false},{"type":"link","label":"IAdminForthDataSourceConnectorBase","href":"/docs/api/types/Back/interfaces/IAdminForthDataSourceConnectorBase","docId":"api/types/Back/interfaces/IAdminForthDataSourceConnectorBase","unlisted":false},{"type":"link","label":"IAdminForthDataSourceConnectorConstructor","href":"/docs/api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor","docId":"api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor","unlisted":false},{"type":"link","label":"IAdminForthFilter","href":"/docs/api/types/Back/interfaces/IAdminForthFilter","docId":"api/types/Back/interfaces/IAdminForthFilter","unlisted":false},{"type":"link","label":"IAdminForthHttpResponse","href":"/docs/api/types/Back/interfaces/IAdminForthHttpResponse","docId":"api/types/Back/interfaces/IAdminForthHttpResponse","unlisted":false},{"type":"link","label":"IAdminForthPlugin","href":"/docs/api/types/Back/interfaces/IAdminForthPlugin","docId":"api/types/Back/interfaces/IAdminForthPlugin","unlisted":false},{"type":"link","label":"IAdminForthSort","href":"/docs/api/types/Back/interfaces/IAdminForthSort","docId":"api/types/Back/interfaces/IAdminForthSort","unlisted":false},{"type":"link","label":"ICodeInjector","href":"/docs/api/types/Back/interfaces/ICodeInjector","docId":"api/types/Back/interfaces/ICodeInjector","unlisted":false},{"type":"link","label":"IConfigValidator","href":"/docs/api/types/Back/interfaces/IConfigValidator","docId":"api/types/Back/interfaces/IConfigValidator","unlisted":false},{"type":"link","label":"IExpressHttpServer","href":"/docs/api/types/Back/interfaces/IExpressHttpServer","docId":"api/types/Back/interfaces/IExpressHttpServer","unlisted":false},{"type":"link","label":"IHttpServer","href":"/docs/api/types/Back/interfaces/IHttpServer","docId":"api/types/Back/interfaces/IHttpServer","unlisted":false},{"type":"link","label":"IOperationalResource","href":"/docs/api/types/Back/interfaces/IOperationalResource","docId":"api/types/Back/interfaces/IOperationalResource","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Type Aliases","items":[{"type":"link","label":"AdminForthConfigMenuItem","href":"/docs/api/types/Back/type-aliases/AdminForthConfigMenuItem","docId":"api/types/Back/type-aliases/AdminForthConfigMenuItem","unlisted":false},{"type":"link","label":"AdminForthDataSource","href":"/docs/api/types/Back/type-aliases/AdminForthDataSource","docId":"api/types/Back/type-aliases/AdminForthDataSource","unlisted":false},{"type":"link","label":"AfterDataSourceResponseFunction","href":"/docs/api/types/Back/type-aliases/AfterDataSourceResponseFunction","docId":"api/types/Back/type-aliases/AfterDataSourceResponseFunction","unlisted":false},{"type":"link","label":"AfterSaveFunction","href":"/docs/api/types/Back/type-aliases/AfterSaveFunction","docId":"api/types/Back/type-aliases/AfterSaveFunction","unlisted":false},{"type":"link","label":"AllowedActionValue","href":"/docs/api/types/Back/type-aliases/AllowedActionValue","docId":"api/types/Back/type-aliases/AllowedActionValue","unlisted":false},{"type":"link","label":"AllowedActions","href":"/docs/api/types/Back/type-aliases/AllowedActions","docId":"api/types/Back/type-aliases/AllowedActions","unlisted":false},{"type":"link","label":"BeforeDataSourceRequestFunction","href":"/docs/api/types/Back/type-aliases/BeforeDataSourceRequestFunction","docId":"api/types/Back/type-aliases/BeforeDataSourceRequestFunction","unlisted":false},{"type":"link","label":"BeforeLoginConfirmationFunction","href":"/docs/api/types/Back/type-aliases/BeforeLoginConfirmationFunction","docId":"api/types/Back/type-aliases/BeforeLoginConfirmationFunction","unlisted":false},{"type":"link","label":"BeforeSaveFunction","href":"/docs/api/types/Back/type-aliases/BeforeSaveFunction","docId":"api/types/Back/type-aliases/BeforeSaveFunction","unlisted":false},{"type":"link","label":"FDataFilter","href":"/docs/api/types/Back/type-aliases/FDataFilter","docId":"api/types/Back/type-aliases/FDataFilter","unlisted":false},{"type":"link","label":"FDataSort","href":"/docs/api/types/Back/type-aliases/FDataSort","docId":"api/types/Back/type-aliases/FDataSort","unlisted":false},{"type":"link","label":"ResourceOptions","href":"/docs/api/types/Back/type-aliases/ResourceOptions","docId":"api/types/Back/type-aliases/ResourceOptions","unlisted":false}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/types/Back/"},{"type":"category","label":"Common","items":[{"type":"category","label":"Enumerations","items":[{"type":"link","label":"ActionCheckSource","href":"/docs/api/types/Common/enumerations/ActionCheckSource","docId":"api/types/Common/enumerations/ActionCheckSource","unlisted":false},{"type":"link","label":"AdminForthDataTypes","href":"/docs/api/types/Common/enumerations/AdminForthDataTypes","docId":"api/types/Common/enumerations/AdminForthDataTypes","unlisted":false},{"type":"link","label":"AdminForthFilterOperators","href":"/docs/api/types/Common/enumerations/AdminForthFilterOperators","docId":"api/types/Common/enumerations/AdminForthFilterOperators","unlisted":false},{"type":"link","label":"AdminForthResourcePages","href":"/docs/api/types/Common/enumerations/AdminForthResourcePages","docId":"api/types/Common/enumerations/AdminForthResourcePages","unlisted":false},{"type":"link","label":"AdminForthSortDirections","href":"/docs/api/types/Common/enumerations/AdminForthSortDirections","docId":"api/types/Common/enumerations/AdminForthSortDirections","unlisted":false},{"type":"link","label":"AllowedActionsEnum","href":"/docs/api/types/Common/enumerations/AllowedActionsEnum","docId":"api/types/Common/enumerations/AllowedActionsEnum","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Interfaces","items":[{"type":"link","label":"AdminForthBulkActionCommon","href":"/docs/api/types/Common/interfaces/AdminForthBulkActionCommon","docId":"api/types/Common/interfaces/AdminForthBulkActionCommon","unlisted":false},{"type":"link","label":"AdminForthColumnEnumItem","href":"/docs/api/types/Common/interfaces/AdminForthColumnEnumItem","docId":"api/types/Common/interfaces/AdminForthColumnEnumItem","unlisted":false},{"type":"link","label":"AdminForthComponentDeclarationFull","href":"/docs/api/types/Common/interfaces/AdminForthComponentDeclarationFull","docId":"api/types/Common/interfaces/AdminForthComponentDeclarationFull","unlisted":false},{"type":"link","label":"AdminForthFieldComponents","href":"/docs/api/types/Common/interfaces/AdminForthFieldComponents","docId":"api/types/Common/interfaces/AdminForthFieldComponents","unlisted":false},{"type":"link","label":"AdminForthForeignResourceCommon","href":"/docs/api/types/Common/interfaces/AdminForthForeignResourceCommon","docId":"api/types/Common/interfaces/AdminForthForeignResourceCommon","unlisted":false},{"type":"link","label":"AdminForthResourceCommon","href":"/docs/api/types/Common/interfaces/AdminForthResourceCommon","docId":"api/types/Common/interfaces/AdminForthResourceCommon","unlisted":false},{"type":"link","label":"AdminUser","href":"/docs/api/types/Common/interfaces/AdminUser","docId":"api/types/Common/interfaces/AdminUser","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Type Aliases","items":[{"type":"link","label":"AdminForthComponentDeclaration","href":"/docs/api/types/Common/type-aliases/AdminForthComponentDeclaration","docId":"api/types/Common/type-aliases/AdminForthComponentDeclaration","unlisted":false},{"type":"link","label":"AdminForthResourceColumnCommon","href":"/docs/api/types/Common/type-aliases/AdminForthResourceColumnCommon","docId":"api/types/Common/type-aliases/AdminForthResourceColumnCommon","unlisted":false},{"type":"link","label":"AllowedActionsResolved","href":"/docs/api/types/Common/type-aliases/AllowedActionsResolved","docId":"api/types/Common/type-aliases/AllowedActionsResolved","unlisted":false},{"type":"link","label":"ValidationObject","href":"/docs/api/types/Common/type-aliases/ValidationObject","docId":"api/types/Common/type-aliases/ValidationObject","unlisted":false}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/types/Common/"},{"type":"category","label":"FrontendAPI","items":[{"type":"category","label":"Enumerations","items":[{"type":"link","label":"AlertVariant","href":"/docs/api/types/FrontendAPI/enumerations/AlertVariant","docId":"api/types/FrontendAPI/enumerations/AlertVariant","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Interfaces","items":[{"type":"link","label":"FrontendAPIInterface","href":"/docs/api/types/FrontendAPI/interfaces/FrontendAPIInterface","docId":"api/types/FrontendAPI/interfaces/FrontendAPIInterface","unlisted":false}],"collapsed":false,"collapsible":false},{"type":"category","label":"Type Aliases","items":[{"type":"link","label":"AlertParams","href":"/docs/api/types/FrontendAPI/type-aliases/AlertParams","docId":"api/types/FrontendAPI/type-aliases/AlertParams","unlisted":false},{"type":"link","label":"ConfirmParams","href":"/docs/api/types/FrontendAPI/type-aliases/ConfirmParams","docId":"api/types/FrontendAPI/type-aliases/ConfirmParams","unlisted":false}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/types/FrontendAPI/"}],"collapsed":false,"collapsible":false}],"collapsed":false,"collapsible":false,"href":"/docs/api/"}]},"docs":{"api/index":{"id":"api/index","title":"TypeDoc API","description":"Modules","sidebar":"apiSidebar"},"api/types/Back/classes/Filters":{"id":"api/types/Back/classes/Filters","title":"Filters","description":"Constructors","sidebar":"apiSidebar"},"api/types/Back/classes/Sorts":{"id":"api/types/Back/classes/Sorts","title":"Sorts","description":"Constructors","sidebar":"apiSidebar"},"api/types/Back/enumerations/AdminForthMenuTypes":{"id":"api/types/Back/enumerations/AdminForthMenuTypes","title":"AdminForthMenuTypes","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Back/index":{"id":"api/types/Back/index","title":"types/Back","description":"Index","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthBulkAction":{"id":"api/types/Back/interfaces/AdminForthBulkAction","title":"AdminForthBulkAction","description":"Extends","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthConfig":{"id":"api/types/Back/interfaces/AdminForthConfig","title":"AdminForthConfig","description":"Main configuration object for AdminForth","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthForeignResource":{"id":"api/types/Back/interfaces/AdminForthForeignResource","title":"AdminForthForeignResource","description":"Extends","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthResource":{"id":"api/types/Back/interfaces/AdminForthResource","title":"AdminForthResource","description":"Resource describes one table or collection in database.","sidebar":"apiSidebar"},"api/types/Back/interfaces/AdminForthResourceColumn":{"id":"api/types/Back/interfaces/AdminForthResourceColumn","title":"AdminForthResourceColumn","description":"Column describes one field in the table or collection in database.","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForth":{"id":"api/types/Back/interfaces/IAdminForth","title":"IAdminForth","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthAuth":{"id":"api/types/Back/interfaces/IAdminForthAuth","title":"IAdminForthAuth","description":"Methods","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthDataSourceConnector":{"id":"api/types/Back/interfaces/IAdminForthDataSourceConnector","title":"IAdminForthDataSourceConnector","description":"Extended by","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthDataSourceConnectorBase":{"id":"api/types/Back/interfaces/IAdminForthDataSourceConnectorBase","title":"IAdminForthDataSourceConnectorBase","description":"Interface that exposes methods to interact with AdminForth in standard way","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor":{"id":"api/types/Back/interfaces/IAdminForthDataSourceConnectorConstructor","title":"IAdminForthDataSourceConnectorConstructor","description":"Constructors","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthFilter":{"id":"api/types/Back/interfaces/IAdminForthFilter","title":"IAdminForthFilter","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthHttpResponse":{"id":"api/types/Back/interfaces/IAdminForthHttpResponse","title":"IAdminForthHttpResponse","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthPlugin":{"id":"api/types/Back/interfaces/IAdminForthPlugin","title":"IAdminForthPlugin","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IAdminForthSort":{"id":"api/types/Back/interfaces/IAdminForthSort","title":"IAdminForthSort","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/ICodeInjector":{"id":"api/types/Back/interfaces/ICodeInjector","title":"ICodeInjector","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/interfaces/IConfigValidator":{"id":"api/types/Back/interfaces/IConfigValidator","title":"IConfigValidator","description":"Methods","sidebar":"apiSidebar"},"api/types/Back/interfaces/IExpressHttpServer":{"id":"api/types/Back/interfaces/IExpressHttpServer","title":"IExpressHttpServer","description":"Implement this interface to create custom HTTP server adapter for AdminForth.","sidebar":"apiSidebar"},"api/types/Back/interfaces/IHttpServer":{"id":"api/types/Back/interfaces/IHttpServer","title":"IHttpServer","description":"Implement this interface to create custom HTTP server adapter for AdminForth.","sidebar":"apiSidebar"},"api/types/Back/interfaces/IOperationalResource":{"id":"api/types/Back/interfaces/IOperationalResource","title":"IOperationalResource","description":"Properties","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AdminForthConfigMenuItem":{"id":"api/types/Back/type-aliases/AdminForthConfigMenuItem","title":"AdminForthConfigMenuItem","description":"AdminForthConfigMenuItem: object","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AdminForthDataSource":{"id":"api/types/Back/type-aliases/AdminForthDataSource","title":"AdminForthDataSource","description":"AdminForthDataSource: object","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AfterDataSourceResponseFunction":{"id":"api/types/Back/type-aliases/AfterDataSourceResponseFunction","title":"AfterDataSourceResponseFunction()","description":"AfterDataSourceResponseFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AfterSaveFunction":{"id":"api/types/Back/type-aliases/AfterSaveFunction","title":"AfterSaveFunction()","description":"AfterSaveFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AllowedActions":{"id":"api/types/Back/type-aliases/AllowedActions","title":"AllowedActions","description":"AllowedActions AllowedActionValue } & object","sidebar":"apiSidebar"},"api/types/Back/type-aliases/AllowedActionValue":{"id":"api/types/Back/type-aliases/AllowedActionValue","title":"AllowedActionValue","description":"AllowedActionValue: boolean \\\\| ({adminUser, resource, meta, source}) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/BeforeDataSourceRequestFunction":{"id":"api/types/Back/type-aliases/BeforeDataSourceRequestFunction","title":"BeforeDataSourceRequestFunction()","description":"BeforeDataSourceRequestFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/BeforeLoginConfirmationFunction":{"id":"api/types/Back/type-aliases/BeforeLoginConfirmationFunction","title":"BeforeLoginConfirmationFunction()","description":"BeforeLoginConfirmationFunction: (params?) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/BeforeSaveFunction":{"id":"api/types/Back/type-aliases/BeforeSaveFunction","title":"BeforeSaveFunction()","description":"BeforeSaveFunction: (params) => Promise\\\\","sidebar":"apiSidebar"},"api/types/Back/type-aliases/FDataFilter":{"id":"api/types/Back/type-aliases/FDataFilter","title":"FDataFilter()","description":"FDataFilter: (field, value) => IAdminForthFilter","sidebar":"apiSidebar"},"api/types/Back/type-aliases/FDataSort":{"id":"api/types/Back/type-aliases/FDataSort","title":"FDataSort()","description":"FDataSort: (field, direction) => IAdminForthSort","sidebar":"apiSidebar"},"api/types/Back/type-aliases/ResourceOptions":{"id":"api/types/Back/type-aliases/ResourceOptions","title":"ResourceOptions","description":"ResourceOptions: Omit \\\\ & object","sidebar":"apiSidebar"},"api/types/Common/enumerations/ActionCheckSource":{"id":"api/types/Common/enumerations/ActionCheckSource","title":"ActionCheckSource","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthDataTypes":{"id":"api/types/Common/enumerations/AdminForthDataTypes","title":"AdminForthDataTypes","description":"Types that are common for both frontend side (SPA) and backend side (server).","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthFilterOperators":{"id":"api/types/Common/enumerations/AdminForthFilterOperators","title":"AdminForthFilterOperators","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthResourcePages":{"id":"api/types/Common/enumerations/AdminForthResourcePages","title":"AdminForthResourcePages","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AdminForthSortDirections":{"id":"api/types/Common/enumerations/AdminForthSortDirections","title":"AdminForthSortDirections","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/enumerations/AllowedActionsEnum":{"id":"api/types/Common/enumerations/AllowedActionsEnum","title":"AllowedActionsEnum","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/Common/index":{"id":"api/types/Common/index","title":"types/Common","description":"Index","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthBulkActionCommon":{"id":"api/types/Common/interfaces/AdminForthBulkActionCommon","title":"AdminForthBulkActionCommon","description":"Extended by","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthColumnEnumItem":{"id":"api/types/Common/interfaces/AdminForthColumnEnumItem","title":"AdminForthColumnEnumItem","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthComponentDeclarationFull":{"id":"api/types/Common/interfaces/AdminForthComponentDeclarationFull","title":"AdminForthComponentDeclarationFull","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthFieldComponents":{"id":"api/types/Common/interfaces/AdminForthFieldComponents","title":"AdminForthFieldComponents","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthForeignResourceCommon":{"id":"api/types/Common/interfaces/AdminForthForeignResourceCommon","title":"AdminForthForeignResourceCommon","description":"Extended by","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminForthResourceCommon":{"id":"api/types/Common/interfaces/AdminForthResourceCommon","title":"AdminForthResourceCommon","description":"Resource describes one table or collection in database.","sidebar":"apiSidebar"},"api/types/Common/interfaces/AdminUser":{"id":"api/types/Common/interfaces/AdminUser","title":"AdminUser","description":"Properties","sidebar":"apiSidebar"},"api/types/Common/type-aliases/AdminForthComponentDeclaration":{"id":"api/types/Common/type-aliases/AdminForthComponentDeclaration","title":"AdminForthComponentDeclaration","description":"AdminForthComponentDeclaration: AdminForthComponentDeclarationFull \\\\| string","sidebar":"apiSidebar"},"api/types/Common/type-aliases/AdminForthResourceColumnCommon":{"id":"api/types/Common/type-aliases/AdminForthResourceColumnCommon","title":"AdminForthResourceColumnCommon","description":"AdminForthResourceColumnCommon: object","sidebar":"apiSidebar"},"api/types/Common/type-aliases/AllowedActionsResolved":{"id":"api/types/Common/type-aliases/AllowedActionsResolved","title":"AllowedActionsResolved","description":"AllowedActionsResolved boolean }","sidebar":"apiSidebar"},"api/types/Common/type-aliases/ValidationObject":{"id":"api/types/Common/type-aliases/ValidationObject","title":"ValidationObject","description":"ValidationObject: object","sidebar":"apiSidebar"},"api/types/FrontendAPI/enumerations/AlertVariant":{"id":"api/types/FrontendAPI/enumerations/AlertVariant","title":"AlertVariant","description":"Enumeration Members","sidebar":"apiSidebar"},"api/types/FrontendAPI/index":{"id":"api/types/FrontendAPI/index","title":"types/FrontendAPI","description":"Index","sidebar":"apiSidebar"},"api/types/FrontendAPI/interfaces/FrontendAPIInterface":{"id":"api/types/FrontendAPI/interfaces/FrontendAPIInterface","title":"FrontendAPIInterface","description":"Properties","sidebar":"apiSidebar"},"api/types/FrontendAPI/type-aliases/AlertParams":{"id":"api/types/FrontendAPI/type-aliases/AlertParams","title":"AlertParams","description":"AlertParams: object","sidebar":"apiSidebar"},"api/types/FrontendAPI/type-aliases/ConfirmParams":{"id":"api/types/FrontendAPI/type-aliases/ConfirmParams","title":"ConfirmParams","description":"ConfirmParams: object","sidebar":"apiSidebar"},"tutorial/Advanced/plugin-development":{"id":"tutorial/Advanced/plugin-development","title":"Plugin development guide","description":"Creating a plugin is a powerful way to extend AdminForth functionality.","sidebar":"tutorialSidebar"},"tutorial/Customization/alert":{"id":"tutorial/Customization/alert","title":"Alerts and confirmations","description":"When you are writing custom components or pages you might need to show alerts or confirmations to the user.","sidebar":"tutorialSidebar"},"tutorial/Customization/branding":{"id":"tutorial/Customization/branding","title":"Branding and Theming","description":"The first things you would probably like to change are the logo, favicon and the name of the application.","sidebar":"tutorialSidebar"},"tutorial/Customization/bulkActions":{"id":"tutorial/Customization/bulkActions","title":"Custom bulk actions","description":"You might need to give admin users a feature to perform same action on multiple records at once.","sidebar":"tutorialSidebar"},"tutorial/Customization/customFieldRendering":{"id":"tutorial/Customization/customFieldRendering","title":"Custom record field rendering","description":"Customizing how AdminForth renders the cells with record values","sidebar":"tutorialSidebar"},"tutorial/Customization/customPages":{"id":"tutorial/Customization/customPages","title":"Custom pages","description":"Most Admin Panels should have some Dashboards or custom pages.","sidebar":"tutorialSidebar"},"tutorial/Customization/dataApi":{"id":"tutorial/Customization/dataApi","title":"Data API","description":"AdminForth Data API is a minimal set of methods to manipulate the data in the database.","sidebar":"tutorialSidebar"},"tutorial/Customization/hooks":{"id":"tutorial/Customization/hooks","title":"Hooks","description":"Hooks are used to:","sidebar":"tutorialSidebar"},"tutorial/Customization/limitingAccess":{"id":"tutorial/Customization/limitingAccess","title":"Limiting actions access","description":"Statically disable some action","sidebar":"tutorialSidebar"},"tutorial/Customization/menuConfiguration":{"id":"tutorial/Customization/menuConfiguration","title":"Menu Configuration","description":"Icons","sidebar":"tutorialSidebar"},"tutorial/Customization/pageInjections":{"id":"tutorial/Customization/pageInjections","title":"Page Injections","description":"In addition to ability to create custom pages and overwrite how fields are rendered, you can also inject custom components in standard AdminForth page.","sidebar":"tutorialSidebar"},"tutorial/Customization/security":{"id":"tutorial/Customization/security","title":"Security","description":"Security and privacy if adminforth users is one of the most important aspects of AdminForth.","sidebar":"tutorialSidebar"},"tutorial/Customization/standardPagesTuning":{"id":"tutorial/Customization/standardPagesTuning","title":"Standard pages tuning","description":"Fields Grouping","sidebar":"tutorialSidebar"},"tutorial/Customization/virtualColumns":{"id":"tutorial/Customization/virtualColumns","title":"Virtual columns","description":"Virtual column for show and list","sidebar":"tutorialSidebar"},"tutorial/deploy":{"id":"tutorial/deploy","title":"Deploy in Docker","description":"In general you can already run your index.ts file which we created in Getting Started","sidebar":"tutorialSidebar"},"tutorial/gettingStarted":{"id":"tutorial/gettingStarted","title":"Getting Started","description":"This Getting Started Page has some explanations and tables with various field types.","sidebar":"tutorialSidebar"},"tutorial/glossary":{"id":"tutorial/glossary","title":"Glossary","description":"dataSource","sidebar":"tutorialSidebar"},"tutorial/helloWorld":{"id":"tutorial/helloWorld","title":"Hello world app","description":"No water. Pure code to get started ASAP.","sidebar":"tutorialSidebar"},"tutorial/Plugins/AuditLog":{"id":"tutorial/Plugins/AuditLog","title":"AuditLog","description":"AuditLog plugin allows to log all changes in the resources done from the admin panel.","sidebar":"tutorialSidebar"},"tutorial/Plugins/chat-gpt":{"id":"tutorial/Plugins/chat-gpt","title":"Chat-GPT","description":"This plugin allows you to auto-complete text and string fields using OpenAI Chat GPT models.","sidebar":"tutorialSidebar"},"tutorial/Plugins/email-password-reset":{"id":"tutorial/Plugins/email-password-reset","title":"Email password reset","description":"Plugin allows to reset password for admin users who forgot their password by sending email with reset link signed with secured JWT token.","sidebar":"tutorialSidebar"},"tutorial/Plugins/ForeignInlineList":{"id":"tutorial/Plugins/ForeignInlineList","title":"ForeignInlineList","description":"Foreign inline list plugin allows to display a list (table) of items from a foreign table in the show view.","sidebar":"tutorialSidebar"},"tutorial/Plugins/import-export":{"id":"tutorial/Plugins/import-export","title":"Import-Export","description":"Import-Export is a plugin that allows you to import and export data from and to a CSV file.","sidebar":"tutorialSidebar"},"tutorial/Plugins/RichEditor":{"id":"tutorial/Plugins/RichEditor","title":"Rich editor","description":"Under the hood this plugin uses Quill. Quill is a free, open source WYSIWYG editor built for the modern web.","sidebar":"tutorialSidebar"},"tutorial/Plugins/TwoFactorsAuth":{"id":"tutorial/Plugins/TwoFactorsAuth","title":"Two-Factor Authentication Plugin","description":"The Two-Factor Authentication Plugin provides an additional layer of security to the application by requiring users to provide a second form of authentication in addition to their password. This plugin supports authenticator apps.","sidebar":"tutorialSidebar"},"tutorial/Plugins/upload":{"id":"tutorial/Plugins/upload","title":"Upload","description":"This plugin allows you to upload files to Amazon S3 bucket.","sidebar":"tutorialSidebar"}}}}')}}]); \ No newline at end of file diff --git a/assets/js/1e563bf9.b1a33881.js b/assets/js/1e563bf9.b1a33881.js new file mode 100644 index 000000000..63e5e1d2b --- /dev/null +++ b/assets/js/1e563bf9.b1a33881.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[7184],{1168:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>p,frontMatter:()=>r,metadata:()=>s,toc:()=>a});var i=t(4848),o=t(8453);const r={},d="Deploy in Docker",s={id:"tutorial/deploy",title:"Deploy in Docker",description:"In general you can already run your index.ts file which we created in Getting Started",source:"@site/docs/tutorial/04-deploy.md",sourceDirName:"tutorial",slug:"/tutorial/deploy",permalink:"/docs/tutorial/deploy",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:4,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"AdminForth Components Library",permalink:"/docs/tutorial/Customization/afcl"},next:{title:"AuditLog",permalink:"/docs/tutorial/Plugins/AuditLog"}},l={},a=[{value:"Building SPA in Docker build time",id:"building-spa-in-docker-build-time",level:2},{value:"Building the image",id:"building-the-image",level:2},{value:"Adding SSL (https) to AdminForth",id:"adding-ssl-https-to-adminforth",level:2},{value:"Subpath deployment",id:"subpath-deployment",level:2}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"deploy-in-docker",children:"Deploy in Docker"}),"\n",(0,i.jsxs)(n.p,{children:["In general you can already run your ",(0,i.jsx)(n.code,{children:"index.ts"})," file which we created in ",(0,i.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"}),"\nwith ",(0,i.jsx)(n.code,{children:"ts-node"})," command in any node environment."]}),"\n",(0,i.jsx)(n.p,{children:"It will start the server on configured HTTP port and you can use any proxy like Traefik/Nginx to expose it to the internet and add SSL Layer."}),"\n",(0,i.jsx)(n.h2,{id:"building-spa-in-docker-build-time",children:"Building SPA in Docker build time"}),"\n",(0,i.jsxs)(n.p,{children:["In current index.ts file you might use call to ",(0,i.jsx)(n.code,{children:"bundleNow"})," method which starts building internal SPA bundle when ",(0,i.jsx)(n.code,{children:"index.ts"})," started\nexecuting. SPA building generally takes from 10 seconds to minute depending on the external modules you will add into AdminForth and extended functionality you will create."]}),"\n",(0,i.jsx)(n.p,{children:"To fully exclude this bundle time we recommend doing bundling in build time."}),"\n",(0,i.jsxs)(n.p,{children:["Create file ",(0,i.jsx)(n.code,{children:"bundleNow.ts"})," in the root directory of your project:"]}),"\n",(0,i.jsx)(n.p,{children:"and put the following code:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./bundleNow.ts'",children:"import { admin } from './index.js';\n\nawait admin.bundleNow({ hotReload: false});\nconsole.log('Bundling AdminForth done.');\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Now completely Remove bundleNow call from ",(0,i.jsx)(n.code,{children:"index.ts"})," file:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./index.ts'",children:"//diff-remove\n // needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime\n//diff-remove\n await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});\n//diff-remove\n console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');\n//diff-add\n if (process.env.NODE_ENV === 'development') {\n//diff-add\n await admin.bundleNow({ hotReload: true });\n//diff-add\n console.log('Bundling AdminForth done');\n//diff-add\n }\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In root directory create file ",(0,i.jsx)(n.code,{children:".dockerignore"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",metastring:"title='./.dockerignore'",children:"node_modules\n*.sqlite\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In root directory create file ",(0,i.jsx)(n.code,{children:"Dockerfile"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-Dockerfile",children:'# use the same node version which you used during dev\nFROM node:20-alpine\nWORKDIR /code/\nADD package.json package-lock.json /code/\nRUN npm ci \nADD . /code/\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\nCMD ["npm", "run", "migrateLiveAndStart"]\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Add ",(0,i.jsx)(n.code,{children:"bundleNow"})," and ",(0,i.jsx)(n.code,{children:"startLive"})," to ",(0,i.jsx)(n.code,{children:"package.json"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./package.json'",children:'{\n "type": "module",\n "scripts": {\n "start": "tsx watch --env-file=.env index.ts",\n//diff-add\n "startLive": "NODE_ENV=production tsx index.ts"\n//diff-add\n "migrate": "npx --yes prisma migrate deploy",\n//diff-add\n "migrateLiveAndStart": "npm run migrate && npm run startLive"\n },\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"building-the-image",children:"Building the image"}),"\n",(0,i.jsx)(n.p,{children:"Now you can build your image:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"docker build -t myadminapp .\n"})}),"\n",(0,i.jsx)(n.p,{children:"And run container with:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"docker run -p 3500:3500 \\\n -e NODE_ENV=production \\\n -e ADMINFORTH_SECRET=CHANGEME \\\n -e DATABASE_FILE=/code/db/db.sqlite \\\n -e DATABASE_FILE_URL=file:/code/db/db.sqlite \\\n -v $(pwd)/db:/code/db \\\n myadminapp\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Now open your browser and go to ",(0,i.jsx)(n.code,{children:"http://localhost:3500"})," to see your AdminForth application running in Docker container."]}),"\n",(0,i.jsx)(n.h2,{id:"adding-ssl-https-to-adminforth",children:"Adding SSL (https) to AdminForth"}),"\n",(0,i.jsx)(n.p,{children:"There are lots of ways today to put your application behind SSL gateway. You might simply put AdminForth instance behind free Cloudflare CDN,\nchange 3500 port to 80 and Cloudflare will automatically add SSL layer and faster CDN for your application."}),"\n",(0,i.jsx)(n.p,{children:"However as a bonus here we will give you independent way to add free LetsEncrypt SSL layer to your AdminForth application."}),"\n",(0,i.jsxs)(n.p,{children:["First move all contents of your root folder (which contains index.ts and other files) to ",(0,i.jsx)(n.code,{children:"app"})," folder:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"mkdir app\nmv {.,}* app\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In root directory create file ",(0,i.jsx)(n.code,{children:"compose.yml"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",metastring:"title='./compose.yml'",children:'version: \'3.8\'\n\nservices:\n traefik:\n image: "traefik:v2.5"\n command:\n - "--api.insecure=true"\n - "--providers.docker=true"\n - "--entrypoints.web.address=:80"\n - "--entrypoints.websecure.address=:443"\n - "--certificatesresolvers.myresolver.acme.httpchallenge=true"\n - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"\n - "--certificatesresolvers.myresolver.acme.email=demo@devforth.io" # \u261d\ufe0f replace with your email\n - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"\n ports:\n - "80:80"\n - "443:443"\n volumes:\n - "/var/run/docker.sock:/var/run/docker.sock:ro"\n - "./letsencrypt:/letsencrypt"\n labels:\n - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"\n - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"\n - "traefik.http.routers.http-catchall.entrypoints=web"\n - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"\n - "traefik.http.routers.http-catchall.tls=false"\n\n adminforth:\n build: ./app\n environment:\n - NODE_ENV=production\n - ADMINFORTH_SECRET=!CHANGEME! # \u261d\ufe0f replace with your secret\n - DATABASE_FILE=/code/db/db.sqlite\n - DATABASE_FILE_URL=file:/code/db/db.sqlite\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.adminforth.tls=true"\n - "traefik.http.routers.adminforth.tls.certresolver=myresolver"\n - "traefik.http.routers.adminforth.rule=PathPrefix(`/`)"\n - "traefik.http.services.adminforth.loadbalancer.server.port=3500"\n - "traefik.http.routers.adminforth.priority=1"\n # needed only if you are using SQLite\n volumes:\n - db:/code/db\n\n# needed only if you are using SQLite\nvolumes:\n db:\n\nnetworks:\n default:\n driver: bridge\n'})}),"\n",(0,i.jsx)(n.p,{children:"Now pull this compose file and all directories to your server and run:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"docker compose -p stack-my-app -f compose.yml up -d --build --remove-orphans --wait\n"})}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:"\u261d\ufe0f You can also test this compose stack locally on your machine but SSL will not work,\nso locally you can ignore Chrome warning about SSL and test your AdminForth application."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"subpath-deployment",children:"Subpath deployment"}),"\n",(0,i.jsxs)(n.p,{children:["If you want to deploy your AdminForth application to a sub-folder like ",(0,i.jsx)(n.code,{children:"https://mydomain.com/admin"})," you\nshould do the following:"]}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Open ",(0,i.jsx)(n.code,{children:"index.ts"})," file and change ",(0,i.jsx)(n.code,{children:"ADMIN_BASE_URL"})," constant to your subpath:"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./index.ts'",children:"//diff-remove\nconst ADMIN_BASE_URL = '';\n//diff-add\nconst ADMIN_BASE_URL = '/admin/';\n"})}),"\n",(0,i.jsxs)(n.ol,{start:"2",children:["\n",(0,i.jsxs)(n.li,{children:["Open ",(0,i.jsx)(n.code,{children:"compose.yml"})," file and change ",(0,i.jsx)(n.code,{children:"traefik.http.routers.adminforth.rule"})," to your subpath:"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yml",metastring:"title='./compose.yml'",children:' ...\n - "traefik.http.routers.adminforth.tls.certresolver=myresolver"\n//diff-remove\n - "traefik.http.routers.adminforth.rule=PathPrefix(`/`)"\n//diff-add\n - "traefik.http.routers.adminforth.rule=PathPrefix(`/admin/`)"\n'})}),"\n",(0,i.jsx)(n.p,{children:"Redeploy compose."}),"\n",(0,i.jsxs)(n.p,{children:["Now you can access your AdminForth application by going to ",(0,i.jsx)(n.code,{children:"https://mydomain.com/admin"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["If you want to automate the deployment process with CI follow ",(0,i.jsx)(n.a,{href:"https://devforth.io/blog/onlogs-open-source-simplified-web-logs-viewer-for-dockers/",children:"our docker - traefik guide"})]}),"\n",(0,i.jsx)(n.h1,{id:"nginx-version",children:"Nginx version"}),"\n",(0,i.jsx)(n.p,{children:"If you are using Nginx instead of traefik, here is siple proxy pass config:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'server {\n listen 80;\n server_name demo.adminforth.dev;\n\n charset utf-8;\n client_max_body_size 75M;\n\n gzip on;\n gzip_disable "msie6";\n gzip_vary on;\n gzip_proxied any;\n gzip_comp_level 8;\n gzip_buffers 16 8k;\n gzip_http_version 1.1;\n gzip_min_length 2000;\n gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;\n\n location / {\n proxy_read_timeout 220s;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header Host $http_host;\n proxy_redirect off;\n proxy_pass http://127.0.0.1:3500;\n }\n}\n'})})]})}function p(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>d,x:()=>s});var i=t(6540);const o={},r=i.createContext(o);function d(e){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:d(e.components),i.createElement(r.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/1e563bf9.bf51305a.js b/assets/js/1e563bf9.bf51305a.js deleted file mode 100644 index 6b8a81fd1..000000000 --- a/assets/js/1e563bf9.bf51305a.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[7184],{1168:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>l,contentTitle:()=>d,default:()=>p,frontMatter:()=>o,metadata:()=>s,toc:()=>a});var i=t(4848),r=t(8453);const o={},d="Deploy in Docker",s={id:"tutorial/deploy",title:"Deploy in Docker",description:"In general you can already run your index.ts file which we created in Getting Started",source:"@site/docs/tutorial/04-deploy.md",sourceDirName:"tutorial",slug:"/tutorial/deploy",permalink:"/docs/tutorial/deploy",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:4,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Standard pages tuning",permalink:"/docs/tutorial/Customization/standardPagesTuning"},next:{title:"AuditLog",permalink:"/docs/tutorial/Plugins/AuditLog"}},l={},a=[{value:"Building SPA in Docker build time",id:"building-spa-in-docker-build-time",level:2},{value:"Building the image",id:"building-the-image",level:2},{value:"Adding SSL (https) to AdminForth",id:"adding-ssl-https-to-adminforth",level:2},{value:"Subpath deployment",id:"subpath-deployment",level:2}];function c(e){const n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",...(0,r.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"deploy-in-docker",children:"Deploy in Docker"}),"\n",(0,i.jsxs)(n.p,{children:["In general you can already run your ",(0,i.jsx)(n.code,{children:"index.ts"})," file which we created in ",(0,i.jsx)(n.a,{href:"/docs/tutorial/gettingStarted",children:"Getting Started"}),"\nwith ",(0,i.jsx)(n.code,{children:"ts-node"})," command in any node environment."]}),"\n",(0,i.jsx)(n.p,{children:"It will start the server on configured HTTP port and you can use any proxy like Traefik/Nginx to expose it to the internet and add SSL Layer."}),"\n",(0,i.jsx)(n.h2,{id:"building-spa-in-docker-build-time",children:"Building SPA in Docker build time"}),"\n",(0,i.jsxs)(n.p,{children:["In current index.ts file you might use call to ",(0,i.jsx)(n.code,{children:"bundleNow"})," method which starts building internal SPA bundle when ",(0,i.jsx)(n.code,{children:"index.ts"})," started\nexecuting. SPA building generally takes from 10 seconds to minute depending on the external modules you will add into AdminForth and extended functionality you will create."]}),"\n",(0,i.jsx)(n.p,{children:"To fully exclude this bundle time we recommend doing bundling in build time."}),"\n",(0,i.jsxs)(n.p,{children:["Create file ",(0,i.jsx)(n.code,{children:"bundleNow.ts"})," in the root directory of your project:"]}),"\n",(0,i.jsx)(n.p,{children:"and put the following code:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./bundleNow.ts'",children:"import { admin } from './index.js';\n\nawait admin.bundleNow({ hotReload: false});\nconsole.log('Bundling AdminForth done.');\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Now completely Remove bundleNow call from ",(0,i.jsx)(n.code,{children:"index.ts"})," file:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./index.ts'",children:"//diff-remove\n // needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime\n//diff-remove\n await admin.bundleNow({ hotReload: process.env.NODE_ENV === 'development'});\n//diff-remove\n console.log('Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.');\n//diff-add\n if (process.env.NODE_ENV === 'development') {\n//diff-add\n await admin.bundleNow({ hotReload: true });\n//diff-add\n console.log('Bundling AdminForth done');\n//diff-add\n }\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In root directory create file ",(0,i.jsx)(n.code,{children:".dockerignore"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",metastring:"title='./.dockerignore'",children:"node_modules\n*.sqlite\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In root directory create file ",(0,i.jsx)(n.code,{children:"Dockerfile"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-Dockerfile",children:'# use the same node version which you used during dev\nFROM node:20-alpine\nWORKDIR /code/\nADD package.json package-lock.json /code/\nRUN npm ci \nADD . /code/\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\nCMD ["npm", "run", "migrateLiveAndStart"]\n'})}),"\n",(0,i.jsxs)(n.p,{children:["Add ",(0,i.jsx)(n.code,{children:"bundleNow"})," and ",(0,i.jsx)(n.code,{children:"startLive"})," to ",(0,i.jsx)(n.code,{children:"package.json"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./package.json'",children:'{\n "type": "module",\n "scripts": {\n "start": "tsx watch --env-file=.env index.ts",\n//diff-add\n "startLive": "NODE_ENV=production tsx index.ts"\n//diff-add\n "migrate": "npx --yes prisma migrate deploy",\n//diff-add\n "migrateLiveAndStart": "npm run migrate && npm run startLive"\n },\n}\n'})}),"\n",(0,i.jsx)(n.h2,{id:"building-the-image",children:"Building the image"}),"\n",(0,i.jsx)(n.p,{children:"Now you can build your image:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"docker build -t myadminapp .\n"})}),"\n",(0,i.jsx)(n.p,{children:"And run container with:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"docker run -p 3500:3500 \\\n -e NODE_ENV=production \\\n -e ADMINFORTH_SECRET=CHANGEME \\\n -e DATABASE_FILE=/code/db/db.sqlite \\\n -e DATABASE_FILE_URL=file:/code/db/db.sqlite \\\n -v $(pwd)/db:/code/db \\\n myadminapp\n"})}),"\n",(0,i.jsxs)(n.p,{children:["Now open your browser and go to ",(0,i.jsx)(n.code,{children:"http://localhost:3500"})," to see your AdminForth application running in Docker container."]}),"\n",(0,i.jsx)(n.h2,{id:"adding-ssl-https-to-adminforth",children:"Adding SSL (https) to AdminForth"}),"\n",(0,i.jsx)(n.p,{children:"There are lots of ways today to put your application behind SSL gateway. You might simply put AdminForth instance behind free Cloudflare CDN,\nchange 3500 port to 80 and Cloudflare will automatically add SSL layer and faster CDN for your application."}),"\n",(0,i.jsx)(n.p,{children:"However as a bonus here we will give you independent way to add free LetsEncrypt SSL layer to your AdminForth application."}),"\n",(0,i.jsxs)(n.p,{children:["First move all contents of your root folder (which contains index.ts and other files) to ",(0,i.jsx)(n.code,{children:"app"})," folder:"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"mkdir app\nmv {.,}* app\n"})}),"\n",(0,i.jsxs)(n.p,{children:["In root directory create file ",(0,i.jsx)(n.code,{children:"compose.yml"}),":"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yaml",metastring:"title='./compose.yml'",children:'version: \'3.8\'\n\nservices:\n traefik:\n image: "traefik:v2.5"\n command:\n - "--api.insecure=true"\n - "--providers.docker=true"\n - "--entrypoints.web.address=:80"\n - "--entrypoints.websecure.address=:443"\n - "--certificatesresolvers.myresolver.acme.httpchallenge=true"\n - "--certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web"\n - "--certificatesresolvers.myresolver.acme.email=demo@devforth.io" # \u261d\ufe0f replace with your email\n - "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"\n ports:\n - "80:80"\n - "443:443"\n volumes:\n - "/var/run/docker.sock:/var/run/docker.sock:ro"\n - "./letsencrypt:/letsencrypt"\n labels:\n - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"\n - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"\n - "traefik.http.routers.http-catchall.entrypoints=web"\n - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"\n - "traefik.http.routers.http-catchall.tls=false"\n\n adminforth:\n build: ./app\n environment:\n - NODE_ENV=production\n - ADMINFORTH_SECRET=!CHANGEME! # \u261d\ufe0f replace with your secret\n - DATABASE_FILE=/code/db/db.sqlite\n - DATABASE_FILE_URL=file:/code/db/db.sqlite\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.adminforth.tls=true"\n - "traefik.http.routers.adminforth.tls.certresolver=myresolver"\n - "traefik.http.routers.adminforth.rule=PathPrefix(`/`)"\n - "traefik.http.services.adminforth.loadbalancer.server.port=3500"\n - "traefik.http.routers.adminforth.priority=1"\n # needed only if you are using SQLite\n volumes:\n - db:/code/db\n\n# needed only if you are using SQLite\nvolumes:\n db:\n\nnetworks:\n default:\n driver: bridge\n'})}),"\n",(0,i.jsx)(n.p,{children:"Now pull this compose file and all directories to your server and run:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:"docker compose -p stack-my-app -f compose.yml up -d --build --remove-orphans --wait\n"})}),"\n",(0,i.jsxs)(n.blockquote,{children:["\n",(0,i.jsx)(n.p,{children:"\u261d\ufe0f You can also test this compose stack locally on your machine but SSL will not work,\nso locally you can ignore Chrome warning about SSL and test your AdminForth application."}),"\n"]}),"\n",(0,i.jsx)(n.h2,{id:"subpath-deployment",children:"Subpath deployment"}),"\n",(0,i.jsxs)(n.p,{children:["If you want to deploy your AdminForth application to a sub-folder like ",(0,i.jsx)(n.code,{children:"https://mydomain.com/admin"})," you\nshould do the following:"]}),"\n",(0,i.jsxs)(n.ol,{children:["\n",(0,i.jsxs)(n.li,{children:["Open ",(0,i.jsx)(n.code,{children:"index.ts"})," file and change ",(0,i.jsx)(n.code,{children:"ADMIN_BASE_URL"})," constant to your subpath:"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-ts",metastring:"title='./index.ts'",children:"//diff-remove\nconst ADMIN_BASE_URL = '';\n//diff-add\nconst ADMIN_BASE_URL = '/admin/';\n"})}),"\n",(0,i.jsxs)(n.ol,{start:"2",children:["\n",(0,i.jsxs)(n.li,{children:["Open ",(0,i.jsx)(n.code,{children:"compose.yml"})," file and change ",(0,i.jsx)(n.code,{children:"traefik.http.routers.adminforth.rule"})," to your subpath:"]}),"\n"]}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-yml",metastring:"title='./compose.yml'",children:' ...\n - "traefik.http.routers.adminforth.tls.certresolver=myresolver"\n//diff-remove\n - "traefik.http.routers.adminforth.rule=PathPrefix(`/`)"\n//diff-add\n - "traefik.http.routers.adminforth.rule=PathPrefix(`/admin/`)"\n'})}),"\n",(0,i.jsx)(n.p,{children:"Redeploy compose."}),"\n",(0,i.jsxs)(n.p,{children:["Now you can access your AdminForth application by going to ",(0,i.jsx)(n.code,{children:"https://mydomain.com/admin"}),"."]}),"\n",(0,i.jsxs)(n.p,{children:["If you want to automate the deployment process with CI follow ",(0,i.jsx)(n.a,{href:"https://devforth.io/blog/onlogs-open-source-simplified-web-logs-viewer-for-dockers/",children:"our docker - traefik guide"})]}),"\n",(0,i.jsx)(n.h1,{id:"nginx-version",children:"Nginx version"}),"\n",(0,i.jsx)(n.p,{children:"If you are using Nginx instead of traefik, here is siple proxy pass config:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{children:'server {\n listen 80;\n server_name demo.adminforth.dev;\n\n charset utf-8;\n client_max_body_size 75M;\n\n gzip on;\n gzip_disable "msie6";\n gzip_vary on;\n gzip_proxied any;\n gzip_comp_level 8;\n gzip_buffers 16 8k;\n gzip_http_version 1.1;\n gzip_min_length 2000;\n gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/vnd.ms-fontobject application/x-font-ttf font/opentype image/svg+xml image/x-icon;\n\n location / {\n proxy_read_timeout 220s;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header Host $http_host;\n proxy_redirect off;\n proxy_pass http://127.0.0.1:3500;\n }\n}\n'})})]})}function p(e={}){const{wrapper:n}={...(0,r.R)(),...e.components};return n?(0,i.jsx)(n,{...e,children:(0,i.jsx)(c,{...e})}):c(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>d,x:()=>s});var i=t(6540);const r={},o=i.createContext(r);function d(e){const n=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:d(e.components),i.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/29a082d0.2a4fd001.js b/assets/js/29a082d0.2a4fd001.js new file mode 100644 index 000000000..eff304bab --- /dev/null +++ b/assets/js/29a082d0.2a4fd001.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[4753],{8249:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>u,frontMatter:()=>i,metadata:()=>o,toc:()=>c});var r=t(4848),s=t(8453);const i={},a="Standard pages tuning",o={id:"tutorial/Customization/standardPagesTuning",title:"Standard pages tuning",description:"Fields Grouping",source:"@site/docs/tutorial/03-Customization/13-standardPagesTuning.md",sourceDirName:"tutorial/03-Customization",slug:"/tutorial/Customization/standardPagesTuning",permalink:"/docs/tutorial/Customization/standardPagesTuning",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:13,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Security",permalink:"/docs/tutorial/Customization/security"},next:{title:"AdminForth Components Library",permalink:"/docs/tutorial/Customization/afcl"}},d={},c=[{value:"Fields Grouping",id:"fields-grouping",level:2},{value:"List",id:"list",level:2},{value:"Default Sorting",id:"default-sorting",level:3},{value:"Page size",id:"page-size",level:3},{value:"Custom row click action",id:"custom-row-click-action",level:3},{value:"Auto-refresh records",id:"auto-refresh-records",level:3},{value:"Creating",id:"creating",level:2},{value:"Fill with default values",id:"fill-with-default-values",level:3}];function l(e){const n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"standard-pages-tuning",children:"Standard pages tuning"}),"\n",(0,r.jsx)(n.h2,{id:"fields-grouping",children:"Fields Grouping"}),"\n",(0,r.jsx)(n.p,{children:'In some cases, you may want to organize data fields into specific groups for better structure and clarity. For example, you could create a "Main Info" group to include columns like title, description, country, and apartment_image. Another group, "Characteristics," could hold attributes such as price, square_meter, number_of_rooms, property_type, and listed. Any values without a specified group will be categorized under "Other."'}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n ...\n options: {\n ...\n //diff-add\n createEditGroups: [\n //diff-add\n {\n //diff-add\n groupName: 'Main info',\n //diff-add\n columns: ['id','title', 'description', 'country', 'apartment_image']\n //diff-add\n },\n //diff-add\n {\n //diff-add\n groupName: 'Characteristics',\n //diff-add\n columns: ['price', 'square_meter', 'number_of_rooms', \"property_type\", \"listed\"]\n //diff-add\n }\n //diff-add\n ],\n }\n }\n ]\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Here is how it looks:\n",(0,r.jsx)(n.img,{alt:"alt text",src:t(5276).A+"",width:"2281",height:"1129"})]}),"\n",(0,r.jsx)(n.h2,{id:"list",children:"List"}),"\n",(0,r.jsx)(n.h3,{id:"default-sorting",children:"Default Sorting"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:"import { AdminForthSortDirections } from 'adminforth';\n\n...\n resources: [\n {\n resourceId: 'aparts',\n options: {\n//diff-add\n defaultSort: {\n//diff-add\n columnName: 'created_at',\n//diff-add\n direction: AdminForthSortDirections.ASC, \n//diff-add\n }\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.h3,{id:"page-size",children:"Page size"}),"\n",(0,r.jsxs)(n.p,{children:["use ",(0,r.jsx)(n.code,{children:"options.listPageSize"})," to define how many records will be shown on the page"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n resourceId: 'aparts',\n options: {\n ...\n//diff-add\n listPageSize: 10,\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.h3,{id:"custom-row-click-action",children:"Custom row click action"}),"\n",(0,r.jsx)(n.p,{children:"By default, when you click on a record in the list view, the show view will be opened."}),"\n",(0,r.jsxs)(n.p,{children:["You can change this behavior by using ",(0,r.jsx)(n.code,{children:"options.listTableClickUrl"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"To disable any action (don't open show) return null:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n resourceId: 'aparts',\n options: {\n ...\n//diff-add\n listTableClickUrl: async (record, adminUser) => null,\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.p,{children:"To open a custom page, return URL to the custom page (can start with https://, or relative adminforth path):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" options: {\n ...\n//diff-add\n listTableClickUrl: async (record, adminUser) => {\n return `https://google.com/search?q=${record.name}`;\n }\n }\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If you wish to open the page in a new tab, add ",(0,r.jsx)(n.code,{children:"target=_blank"})," get param to the returned URL:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" options: {\n ...\n//diff-add\n listTableClickUrl: async (record, adminUser) => {\n return `https://google.com/search?q=${record.name}&target=_blank`;\n }\n }\n"})}),"\n",(0,r.jsx)(n.h3,{id:"auto-refresh-records",children:"Auto-refresh records"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"options.listRowsAutoRefreshSeconds"})," might be used to silently refresh records that are loaded (no new records will be fetched if\nthey appear)"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n resourceId: 'aparts',\n hooks: {\n//diff-add\n list: {\n//diff-add\n afterDatasourceResponse: async ({ response }: { response: any }) => { \n//diff-add\n response.forEach((r: any) => {\n//diff-add\n // substitute random country on any load\n//diff-add\n const countries = [ 'US', 'DE', 'FR', 'GB', 'NL', 'IT', 'ES', 'DK', 'PL', 'UA', \n//diff-add\n 'CA', 'AU', 'BR', 'JP', 'CN', 'IN', 'KR', 'TR', 'MX', 'ID']\n//diff-add\n r.country = countries[Math.floor(Math.random() * countries.length)];\n//diff-add\n })\n//diff-add\n return { ok: true, error: \"\" }\n//diff-add\n }\n//diff-add\n }\n },\n options: {\n ...\n//diff-add\n listRowsAutoRefreshSeconds: 1,\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"alt text",src:t(8481).A+"",width:"1999",height:"1499"})}),"\n",(0,r.jsx)(n.h2,{id:"creating",children:"Creating"}),"\n",(0,r.jsx)(n.h3,{id:"fill-with-default-values",children:"Fill with default values"}),"\n",(0,r.jsx)(n.p,{children:"Sometimes you want to generate some field value without asking user to fill it. For example createdAt oftenly store time of creation of the record. You can do this by using fillOnCreate:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts" ',children:"\nnew AdminForth({\n ...\n resources: [\n {\n name: 'appartments',\n fields: [\n ...\n {\n name: 'created_at',\n type: AdminForthDataTypes.DATETIME,\n//diff-add\n fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),\n },\n ],\n },\n ...\n ],\n"})}),"\n",(0,r.jsx)(n.p,{children:"Also you can assign adminUser ID by adminUser.dbUser.id in same hook:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:"new AdminForth({\n ...\n resources: [\n {\n name: 'appartments',\n fields: [\n ...\n {\n name: 'created_by',\n type: AdminForthDataTypes.STRING,\n//diff-add\n fillOnCreate: ({ initialRecord, adminUser }) => adminUser.dbUser.id,\n },\n ],\n },\n ...\n ],\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Same effect can be achieved by using ",(0,r.jsx)(n.a,{href:"/docs/tutorial/Customization/hooks/#modify-the-data-before-it-is-saved-to-the-database",children:"hooks"}),". But ",(0,r.jsx)(n.code,{children:"fillOnCreate"})," might be shorter and more readable."]}),"\n"]})]})}function u(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},5276:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/createEditGroups-f18aa6d458361110bb74ccf4de8d7bb1.png"},8481:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/silent refresh-f8dfa7102e3a1d31345a43ece682ba02.gif"},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>o});var r=t(6540);const s={},i=r.createContext(s);function a(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function o(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/29a082d0.ce1e9ac4.js b/assets/js/29a082d0.ce1e9ac4.js deleted file mode 100644 index a551ffc3e..000000000 --- a/assets/js/29a082d0.ce1e9ac4.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[4753],{8249:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>o,contentTitle:()=>a,default:()=>u,frontMatter:()=>i,metadata:()=>d,toc:()=>c});var r=t(4848),s=t(8453);const i={},a="Standard pages tuning",d={id:"tutorial/Customization/standardPagesTuning",title:"Standard pages tuning",description:"Fields Grouping",source:"@site/docs/tutorial/03-Customization/13-standardPagesTuning.md",sourceDirName:"tutorial/03-Customization",slug:"/tutorial/Customization/standardPagesTuning",permalink:"/docs/tutorial/Customization/standardPagesTuning",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:13,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Security",permalink:"/docs/tutorial/Customization/security"},next:{title:"Deploy in Docker",permalink:"/docs/tutorial/deploy"}},o={},c=[{value:"Fields Grouping",id:"fields-grouping",level:2},{value:"List",id:"list",level:2},{value:"Default Sorting",id:"default-sorting",level:3},{value:"Page size",id:"page-size",level:3},{value:"Custom row click action",id:"custom-row-click-action",level:3},{value:"Auto-refresh records",id:"auto-refresh-records",level:3},{value:"Creating",id:"creating",level:2},{value:"Fill with default values",id:"fill-with-default-values",level:3}];function l(e){const n={a:"a",blockquote:"blockquote",code:"code",h1:"h1",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"standard-pages-tuning",children:"Standard pages tuning"}),"\n",(0,r.jsx)(n.h2,{id:"fields-grouping",children:"Fields Grouping"}),"\n",(0,r.jsx)(n.p,{children:'In some cases, you may want to organize data fields into specific groups for better structure and clarity. For example, you could create a "Main Info" group to include columns like title, description, country, and apartment_image. Another group, "Characteristics," could hold attributes such as price, square_meter, number_of_rooms, property_type, and listed. Any values without a specified group will be categorized under "Other."'}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n ...\n options: {\n ...\n //diff-add\n createEditGroups: [\n //diff-add\n {\n //diff-add\n groupName: 'Main info',\n //diff-add\n columns: ['id','title', 'description', 'country', 'apartment_image']\n //diff-add\n },\n //diff-add\n {\n //diff-add\n groupName: 'Characteristics',\n //diff-add\n columns: ['price', 'square_meter', 'number_of_rooms', \"property_type\", \"listed\"]\n //diff-add\n }\n //diff-add\n ],\n }\n }\n ]\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Here is how it looks:\n",(0,r.jsx)(n.img,{alt:"alt text",src:t(5276).A+"",width:"2281",height:"1129"})]}),"\n",(0,r.jsx)(n.h2,{id:"list",children:"List"}),"\n",(0,r.jsx)(n.h3,{id:"default-sorting",children:"Default Sorting"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:"import { AdminForthSortDirections } from 'adminforth';\n\n...\n resources: [\n {\n resourceId: 'aparts',\n options: {\n//diff-add\n defaultSort: {\n//diff-add\n columnName: 'created_at',\n//diff-add\n direction: AdminForthSortDirections.ASC, \n//diff-add\n }\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.h3,{id:"page-size",children:"Page size"}),"\n",(0,r.jsxs)(n.p,{children:["use ",(0,r.jsx)(n.code,{children:"options.listPageSize"})," to define how many records will be shown on the page"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n resourceId: 'aparts',\n options: {\n ...\n//diff-add\n listPageSize: 10,\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.h3,{id:"custom-row-click-action",children:"Custom row click action"}),"\n",(0,r.jsx)(n.p,{children:"By default, when you click on a record in the list view, the show view will be opened."}),"\n",(0,r.jsxs)(n.p,{children:["You can change this behavior by using ",(0,r.jsx)(n.code,{children:"options.listTableClickUrl"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"To disable any action (don't open show) return null:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n resourceId: 'aparts',\n options: {\n ...\n//diff-add\n listTableClickUrl: async (record, adminUser) => null,\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.p,{children:"To open a custom page, return URL to the custom page (can start with https://, or relative adminforth path):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" options: {\n ...\n//diff-add\n listTableClickUrl: async (record, adminUser) => {\n return `https://google.com/search?q=${record.name}`;\n }\n }\n"})}),"\n",(0,r.jsxs)(n.p,{children:["If you wish to open the page in a new tab, add ",(0,r.jsx)(n.code,{children:"target=_blank"})," get param to the returned URL:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" options: {\n ...\n//diff-add\n listTableClickUrl: async (record, adminUser) => {\n return `https://google.com/search?q=${record.name}&target=_blank`;\n }\n }\n"})}),"\n",(0,r.jsx)(n.h3,{id:"auto-refresh-records",children:"Auto-refresh records"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"options.listRowsAutoRefreshSeconds"})," might be used to silently refresh records that are loaded (no new records will be fetched if\nthey appear)"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:" resources: [\n {\n resourceId: 'aparts',\n hooks: {\n//diff-add\n list: {\n//diff-add\n afterDatasourceResponse: async ({ response }: { response: any }) => { \n//diff-add\n response.forEach((r: any) => {\n//diff-add\n // substitute random country on any load\n//diff-add\n const countries = [ 'US', 'DE', 'FR', 'GB', 'NL', 'IT', 'ES', 'DK', 'PL', 'UA', \n//diff-add\n 'CA', 'AU', 'BR', 'JP', 'CN', 'IN', 'KR', 'TR', 'MX', 'ID']\n//diff-add\n r.country = countries[Math.floor(Math.random() * countries.length)];\n//diff-add\n })\n//diff-add\n return { ok: true, error: \"\" }\n//diff-add\n }\n//diff-add\n }\n },\n options: {\n ...\n//diff-add\n listRowsAutoRefreshSeconds: 1,\n }\n }\n ]\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"alt text",src:t(8481).A+"",width:"1999",height:"1499"})}),"\n",(0,r.jsx)(n.h2,{id:"creating",children:"Creating"}),"\n",(0,r.jsx)(n.h3,{id:"fill-with-default-values",children:"Fill with default values"}),"\n",(0,r.jsx)(n.p,{children:"Sometimes you want to generate some field value without asking user to fill it. For example createdAt oftenly store time of creation of the record. You can do this by using fillOnCreate:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts" ',children:"\nnew AdminForth({\n ...\n resources: [\n {\n name: 'appartments',\n fields: [\n ...\n {\n name: 'created_at',\n type: AdminForthDataTypes.DATETIME,\n//diff-add\n fillOnCreate: ({ initialRecord, adminUser }) => (new Date()).toISOString(),\n },\n ],\n },\n ...\n ],\n"})}),"\n",(0,r.jsx)(n.p,{children:"Also you can assign adminUser ID by adminUser.dbUser.id in same hook:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-typescript",metastring:'title="./resources/appartments.ts"',children:"new AdminForth({\n ...\n resources: [\n {\n name: 'appartments',\n fields: [\n ...\n {\n name: 'created_by',\n type: AdminForthDataTypes.STRING,\n//diff-add\n fillOnCreate: ({ initialRecord, adminUser }) => adminUser.dbUser.id,\n },\n ],\n },\n ...\n ],\n"})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["Same effect can be achieved by using ",(0,r.jsx)(n.a,{href:"/docs/tutorial/Customization/hooks/#modify-the-data-before-it-is-saved-to-the-database",children:"hooks"}),". But ",(0,r.jsx)(n.code,{children:"fillOnCreate"})," might be shorter and more readable."]}),"\n"]})]})}function u(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(l,{...e})}):l(e)}},5276:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/createEditGroups-f18aa6d458361110bb74ccf4de8d7bb1.png"},8481:(e,n,t)=>{t.d(n,{A:()=>r});const r=t.p+"assets/images/silent refresh-f8dfa7102e3a1d31345a43ece682ba02.gif"},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>d});var r=t(6540);const s={},i=r.createContext(s);function a(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function d(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7b1edb8e.8069f3ec.js b/assets/js/7b1edb8e.8069f3ec.js new file mode 100644 index 000000000..6594fa8a3 --- /dev/null +++ b/assets/js/7b1edb8e.8069f3ec.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[6961],{8221:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>p,frontMatter:()=>o,metadata:()=>i,toc:()=>l});var r=t(4848),s=t(8453);const o={slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},a=void 0,i={permalink:"/blog/compose-ec2-deployment-github-actions",source:"@site/blog/2024-11-14-compose-ec2-deployment-ci/index.md",title:"Deploy AdminForth to EC2 with terraform on CI",description:"Here is more advanced snippet to deploy AdminForth to Terraform.",date:"2024-11-14T00:00:00.000Z",tags:[{inline:!1,label:"AWS",permalink:"/blog/tags/aws",description:"Amazon Web Services (AWS) is a cloud computing platform that provides a wide range of services for building and deploying applications."},{inline:!1,label:"Terraform",permalink:"/blog/tags/terraform",description:"Terraform is an open-source infrastructure as code software tool created by HashiCorp that enables users to define and provision data center infrastructure using a declarative configuration language."},{inline:!1,label:"GitHub Actions",permalink:"/blog/tags/github-actions",description:"GitHub Actions is a continuous integration and continuous deployment (CI/CD) service provided by GitHub that allows you to automate your software development workflows."}],readingTime:7.17,hasTruncateMarker:!1,authors:[{name:"Ivan Borshcho",title:"Maintainer of AdminForth",url:"https://github.com/ivictbor",imageURL:"https://avatars.githubusercontent.com/u/1838656?v=4",key:"ivanb"}],frontMatter:{slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},unlisted:!1,nextItem:{title:"Deploy AdminForth to EC2 with terraform (without CI)",permalink:"/blog/compose-ec2-deployment"}},c={authorsImageUrls:[void 0]},l=[{value:"Step 1 - Dockerfile",id:"step-1---dockerfile",level:2},{value:"Step 2 - compose.yml",id:"step-2---composeyml",level:2},{value:"Step 3 - create a SSH keypair",id:"step-3---create-a-ssh-keypair",level:2},{value:"Step 4 - .gitignore file",id:"step-4---gitignore-file",level:2},{value:"Step 5 - Main terraform file main.tf",id:"step-5---main-terraform-file-maintf",level:2},{value:"Step 5.1 - Configure AWS Profile",id:"step-51---configure-aws-profile",level:3},{value:"Step 5.2 - Run deployment",id:"step-52---run-deployment",level:3},{value:"Step 6 - Migrate state to the cloud",id:"step-6---migrate-state-to-the-cloud",level:2},{value:"Step 7 - CI/CD - Github Actions",id:"step-7---cicd---github-actions",level:2},{value:"Step 7.1 - Create deploy script",id:"step-71---create-deploy-script",level:3},{value:"Step 7.2 - Add secrets to GitHub",id:"step-72---add-secrets-to-github",level:3}];function d(e){const n={blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"Here is more advanced snippet to deploy AdminForth to Terraform."}),"\n",(0,r.jsx)(n.p,{children:"Here Terraform state will be stored in the cloud, so you can run this deployment from any machine including stateless CI/CD."}),"\n",(0,r.jsx)(n.p,{children:"We will use GitHub Actions as CI/CD, but you can use any other CI/CD, for example self-hosted free WoodpeckerCI."}),"\n",(0,r.jsxs)(n.p,{children:["Assume you have your AdminForth project in ",(0,r.jsx)(n.code,{children:"myadmin"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"step-1---dockerfile",children:"Step 1 - Dockerfile"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"Dockerfile"})," in ",(0,r.jsx)(n.code,{children:"myadmin"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-Dockerfile",metastring:'title="./myadmin/Dockerfile"',children:'# use the same node version which you used during dev\nFROM node:20-alpine\nWORKDIR /code/\nADD package.json package-lock.json /code/\nRUN npm ci \nADD . /code/\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\nCMD ["npm", "run", "startLive"]\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-2---composeyml",children:"Step 2 - compose.yml"}),"\n",(0,r.jsxs)(n.p,{children:["create folder ",(0,r.jsx)(n.code,{children:"deploy"})," and create file ",(0,r.jsx)(n.code,{children:"compose.yml"})," inside:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title="deploy/compose.yml"',children:'\nservices:\n traefik:\n image: "traefik:v2.5"\n command:\n - "--api.insecure=true"\n - "--providers.docker=true"\n - "--entrypoints.web.address=:80"\n ports:\n - "80:80"\n volumes:\n - "/var/run/docker.sock:/var/run/docker.sock:ro"\n\n myadmin:\n build: ./myadmin\n restart: always\n env_file:\n - ./myadmin/.env\n volumes:\n - myadmin-db:/code/db\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.myadmin.rule=PathPrefix(`/`)"\n - "traefik.http.services.myadmin.loadbalancer.server.port=3500"\n - "traefik.http.routers.myadmin.priority=2"\n\nvolumes:\n myadmin-db:\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-3---create-a-ssh-keypair",children:"Step 3 - create a SSH keypair"}),"\n",(0,r.jsxs)(n.p,{children:["Make sure you are in ",(0,r.jsx)(n.code,{children:"deploy"})," folder, run next command here:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy"',children:'mkdir .keys && ssh-keygen -f .keys/id_rsa -N ""\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Now it should create ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa"})," and ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa.pub"})," files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually."]}),"\n",(0,r.jsx)(n.h2,{id:"step-4---gitignore-file",children:"Step 4 - .gitignore file"}),"\n",(0,r.jsxs)(n.p,{children:["Create ",(0,r.jsx)(n.code,{children:"deploy/.gitignore"})," file with next content:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:".terraform/\n.keys/\n*.tfstate\n*.tfstate.*\n*.tfvars\ntfplan\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-5---main-terraform-file-maintf",children:"Step 5 - Main terraform file main.tf"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"main.tf"})," in ",(0,r.jsx)(n.code,{children:"deploy"})," folder:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="deploy/main.tf"',children:'\nlocals {\n app_name = ""\n aws_region = "eu-central-1"\n}\n\n\nprovider "aws" {\n region = local.aws_region\n profile = "myaws"\n}\n\ndata "aws_ami" "ubuntu_linux" {\n most_recent = true\n owners = ["amazon"]\n\n filter {\n name = "name"\n values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]\n }\n}\n\ndata "aws_vpc" "default" {\n default = true\n}\n\n\nresource "aws_eip" "eip" {\n vpc = true\n}\nresource "aws_eip_association" "eip_assoc" {\n instance_id = aws_instance.app_instance.id\n allocation_id = aws_eip.eip.id\n}\n\ndata "aws_subnet" "default_subnet" {\n filter {\n name = "vpc-id"\n values = [data.aws_vpc.default.id]\n }\n\n filter {\n name = "default-for-az"\n values = ["true"]\n }\n\n filter {\n name = "availability-zone"\n values = ["${local.aws_region}a"]\n }\n}\n\nresource "aws_security_group" "instance_sg" {\n name = "${local.app_name}-instance-sg"\n vpc_id = data.aws_vpc.default.id\n\n ingress {\n description = "Allow HTTP"\n from_port = 80\n to_port = 80\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n # SSH\n ingress {\n description = "Allow SSH"\n from_port = 22\n to_port = 22\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n egress {\n description = "Allow all outbound traffic"\n from_port = 0\n to_port = 0\n protocol = "-1"\n cidr_blocks = ["0.0.0.0/0"]\n }\n}\n\nresource "aws_key_pair" "app_deployer" {\n key_name = "terraform-deploy_${local.app_name}-key"\n public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key\n}\n\nresource "aws_instance" "app_instance" {\n ami = data.aws_ami.ubuntu_linux.id\n instance_type = "t3a.small"\n subnet_id = data.aws_subnet.default_subnet.id\n vpc_security_group_ids = [aws_security_group.instance_sg.id]\n key_name = aws_key_pair.app_deployer.key_name\n\n root_block_device {\n volume_size = 40 // Size in GB for root partition\n volume_type = "gp2"\n }\n\n user_data = <<-EOF\n #!/bin/bash\n sudo apt-get update\n sudo apt-get install ca-certificates curl\n sudo install -m 0755 -d /etc/apt/keyrings\n sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\n sudo chmod a+r /etc/apt/keyrings/docker.asc\n\n # Add the repository to Apt sources:\n echo \\\n "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\\n $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \\\n sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\n sudo apt-get update\n\n sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\n systemctl start docker\n systemctl enable docker\n usermod -a -G docker ubuntu\n EOF\n\n tags = {\n Name = "${local.app_name}-instance"\n }\n}\n\nresource "null_resource" "sync_files_and_run" {\n # Use rsync to exclude node_modules, .git, db\n provisioner "local-exec" {\n # heredoc syntax\n # remove files that where deleted on the source\n command = <<-EOF\n # -o StrictHostKeyChecking=no\n rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \\\n --delete \\\n --exclude \'node_modules\' \\\n --exclude \'.git\' \\\n --exclude \'.terraform\' \\\n --exclude \'terraform*\' \\\n --exclude \'tfplan\' \\\n --exclude \'.keys\' \\\n --exclude \'.vscode\' \\\n --exclude \'.env\' \\\n --exclude \'db\' \\\n --exclude \'up-human/debug\' \\\n --exclude \'up-human/storage\' \\\n ../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/\n EOF\n }\n\n # Run docker compose after files have been copied\n provisioner "remote-exec" {\n inline = [\n # fail bash specially and intentionally to stop the script on error\n "bash -c \'while ! command -v docker &> /dev/null; do echo \\"Waiting for Docker to be installed...\\"; sleep 1; done\'",\n "bash -c \'while ! docker info &> /dev/null; do echo \\"Waiting for Docker to start...\\"; sleep 1; done\'",\n \n # please note that prune might destroy build cache and make build slower, however it releases disk space\n "docker system prune -f",\n # "docker buildx prune -f --filter \'type!=exec.cachemount\'",\n "cd /home/ubuntu/app/deploy",\n "docker compose -p app -f compose.yml up --build -d"\n ]\n\n connection {\n type = "ssh"\n user = "ubuntu"\n private_key = file("./.keys/id_rsa")\n host = aws_eip_association.eip_assoc.public_ip\n }\n }\n\n # Ensure the resource is triggered every time based on timestamp or file hash\n triggers = {\n always_run = timestamp()\n }\n\n depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]\n}\n\n\noutput "instance_public_ip" {\n value = aws_eip_association.eip_assoc.public_ip\n}\n\n\n######### This scetion is for tf state storage ##############\n\n# S3 bucket for storing Terraform state\nresource "aws_s3_bucket" "terraform_state" {\n bucket = "${local.app_name}-terraform-state"\n}\n\nresource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n status = "Enabled"\n id = "Keep only the latest version of the state file"\n\n noncurrent_version_expiration {\n noncurrent_days = 30\n }\n }\n}\n\nresource "aws_s3_bucket_versioning" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n versioning_configuration {\n status = "Enabled"\n }\n}\n\nresource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = "AES256"\n }\n }\n}\n\n# DynamoDB table for state locking\nresource "aws_dynamodb_table" "terraform_lock" {\n name = "${local.app_name}-terraform-lock-table"\n billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand\n\n hash_key = "LockID" # Primary key for the table\n\n attribute {\n name = "LockID"\n type = "S"\n }\n}\n\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters)"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"step-51---configure-aws-profile",children:"Step 5.1 - Configure AWS Profile"}),"\n",(0,r.jsx)(n.p,{children:"Open or create file ~/.aws/credentials and add (if not already there):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"[myaws]\naws_access_key_id = \naws_secret_access_key = \n"})}),"\n",(0,r.jsx)(n.h3,{id:"step-52---run-deployment",children:"Step 5.2 - Run deployment"}),"\n",(0,r.jsx)(n.p,{children:"To run the deployment first time, you need to run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run deployement:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-6---migrate-state-to-the-cloud",children:"Step 6 - Migrate state to the cloud"}),"\n",(0,r.jsx)(n.p,{children:"First deployment had to create S3 bucket and DynamoDB table for storing Terraform state. Now we need to migrate the state to the cloud."}),"\n",(0,r.jsxs)(n.p,{children:["Add to the end of ",(0,r.jsx)(n.code,{children:"main.tf"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="main.tf"',children:'\n# Configure the backend to use the S3 bucket and DynamoDB table\nterraform {\n backend "s3" {\n bucket = "-terraform-state"\n key = "state.tfstate" # Define a specific path for the state file\n region = "eu-central-1"\n profile = "myaws"\n dynamodb_table = "-terraform-lock-table"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters).\nUnfortunately we can't use variables, HashiCorp thinks it is too dangerous \ud83d\ude25"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init -migrate-state\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run test deployment:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-7---cicd---github-actions",children:"Step 7 - CI/CD - Github Actions"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:".github/workflows/deploy.yml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title=".github/workflows/deploy.yml"',children:'name: Deploy \nrun-name: ${{ github.actor }} builds app \ud83d\ude80\non: [push]\njobs:\n Explore-GitHub-Actions:\n runs-on: ubuntu-latest\n steps:\n - run: echo "\ud83c\udf89 The job was automatically triggered by a ${{ github.event_name }} event."\n - run: echo "\ud83d\udc27 This job is now running on a ${{ runner.os }} server"\n - run: echo "\ud83d\udd0e The name of your branch is ${{ github.ref }}"\n - name: Check out repository code\n uses: actions/checkout@v4\n - name: Set up Terraform\n uses: hashicorp/setup-terraform@v2\n with:\n terraform_version: 1.4.6 \n - run: echo "\ud83d\udca1 The ${{ github.repository }} repository has been cloned to the runner."\n - name: Start building\n env:\n VAULT_AWS_ACCESS_KEY_ID: ${{ secrets.VAULT_AWS_ACCESS_KEY_ID }}\n VAULT_AWS_SECRET_ACCESS_KEY: ${{ secrets.VAULT_AWS_SECRET_ACCESS_KEY }}\n VAULT_SSH_PRIVATE_KEY: ${{ secrets.VAULT_SSH_PRIVATE_KEY }}\n VAULT_SSH_PUBLIC_KEY: ${{ secrets.VAULT_SSH_PUBLIC_KEY }}\n run: |\n /bin/sh -x deploy/deploy.sh\n \n - run: echo "\ud83c\udf4f This job\'s status is ${{ job.status }}."\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-71---create-deploy-script",children:"Step 7.1 - Create deploy script"}),"\n",(0,r.jsxs)(n.p,{children:["Now create file ",(0,r.jsx)(n.code,{children:"deploy/deploy.sh"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy/deploy.sh"',children:'\n# cd to dir of script\ncd "$(dirname "$0")"\n\nmkdir -p ~/.aws ./.keys\n\ncat < ~/.aws/credentials\n[myaws]\naws_access_key_id=$VAULT_AWS_ACCESS_KEY_ID\naws_secret_access_key=$VAULT_AWS_SECRET_ACCESS_KEY\nEOF\n\ncat < ./.keys/id_rsa\n$VAULT_SSH_PRIVATE_KEY\nEOF\n\ncat < ./.keys/id_rsa.pub\n$VAULT_SSH_PUBLIC_KEY\nEOF\n\nchmod 600 ./.keys/id_rsa*\n\n# force Terraform to reinitialize the backend without migrating the state.\nterraform init -reconfigure\nterraform plan -out=tfplan\nterraform apply tfplan\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-72---add-secrets-to-github",children:"Step 7.2 - Add secrets to GitHub"}),"\n",(0,r.jsxs)(n.p,{children:["Go to your GitHub repository, then ",(0,r.jsx)(n.code,{children:"Settings"})," -> ",(0,r.jsx)(n.code,{children:"Secrets"})," -> ",(0,r.jsx)(n.code,{children:"New repository secret"})," and add:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_ACCESS_KEY_ID"})," - your AWS access key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_SECRET_ACCESS_KEY"})," - your AWS secret key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PRIVATE_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa"})," and paste to GitHub secrets"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PUBLIC_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa.pub"})," and paste to GitHub secrets"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now you can push your changes to GitHub and see how it will be deployed automatically."})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>i});var r=t(6540);const s={},o=r.createContext(s);function a(e){const n=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/7b1edb8e.c58d40a9.js b/assets/js/7b1edb8e.c58d40a9.js deleted file mode 100644 index fe5147ed4..000000000 --- a/assets/js/7b1edb8e.c58d40a9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[6961],{8221:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>p,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var r=t(4848),s=t(8453);const a={slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},o=void 0,i={permalink:"/blog/compose-ec2-deployment-github-actions",source:"@site/blog/2024-11-14-compose-ec2-deployment-ci/index.md",title:"Deploy AdminForth to EC2 with terraform on CI",description:"Here is more advanced snippet to deploy AdminForth to Terraform.",date:"2024-11-14T00:00:00.000Z",tags:[{inline:!1,label:"AWS",permalink:"/blog/tags/aws",description:"Amazon Web Services (AWS) is a cloud computing platform that provides a wide range of services for building and deploying applications."},{inline:!1,label:"Terraform",permalink:"/blog/tags/terraform",description:"Terraform is an open-source infrastructure as code software tool created by HashiCorp that enables users to define and provision data center infrastructure using a declarative configuration language."},{inline:!1,label:"GitHub Actions",permalink:"/blog/tags/github-actions",description:"GitHub Actions is a continuous integration and continuous deployment (CI/CD) service provided by GitHub that allows you to automate your software development workflows."}],readingTime:7.155,hasTruncateMarker:!1,authors:[{name:"Ivan Borshcho",title:"Maintainer of AdminForth",url:"https://github.com/ivictbor",imageURL:"https://avatars.githubusercontent.com/u/1838656?v=4",key:"ivanb"}],frontMatter:{slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},unlisted:!1,nextItem:{title:"Deploy AdminForth to EC2 with terraform (without CI)",permalink:"/blog/compose-ec2-deployment"}},c={authorsImageUrls:[void 0]},l=[{value:"Step 1 - Dockerfile",id:"step-1---dockerfile",level:2},{value:"Step 2 - compose.yml",id:"step-2---composeyml",level:2},{value:"Step 3 - create a SSH keypair",id:"step-3---create-a-ssh-keypair",level:2},{value:"Step 4 - .gitignore file",id:"step-4---gitignore-file",level:2},{value:"Step 5 - Main terraform file main.tf",id:"step-5---main-terraform-file-maintf",level:2},{value:"Step 5.1 - Configure AWS Profile",id:"step-51---configure-aws-profile",level:3},{value:"Step 5.2 - Run deployment",id:"step-52---run-deployment",level:3},{value:"Step 6 - Migrate state to the cloud",id:"step-6---migrate-state-to-the-cloud",level:2},{value:"Step 7 - CI/CD - Github Actions",id:"step-7---cicd---github-actions",level:2},{value:"Step 6.1 - Create deploy script",id:"step-61---create-deploy-script",level:3},{value:"Step 6.2 - Add secrets to GitHub",id:"step-62---add-secrets-to-github",level:3}];function d(e){const n={blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"Here is more advanced snippet to deploy AdminForth to Terraform."}),"\n",(0,r.jsx)(n.p,{children:"Here Terraform state will be stored in the cloud, so you can run this deployment from any machine including stateless CI/CD."}),"\n",(0,r.jsx)(n.p,{children:"We will use GitHub Actions as CI/CD, but you can use any other CI/CD, for example self-hosted free WoodpeckerCI."}),"\n",(0,r.jsxs)(n.p,{children:["Assume you have your AdminForth project in ",(0,r.jsx)(n.code,{children:"myadmin"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"step-1---dockerfile",children:"Step 1 - Dockerfile"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"Dockerfile"})," in ",(0,r.jsx)(n.code,{children:"myadmin"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-Dockerfile",metastring:'title="./myadmin/Dockerfile"',children:'# use the same node version which you used during dev\nFROM node:20-alpine\nWORKDIR /code/\nADD package.json package-lock.json /code/\nRUN npm ci \nADD . /code/\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\nCMD ["npm", "run", "startLive"]\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-2---composeyml",children:"Step 2 - compose.yml"}),"\n",(0,r.jsxs)(n.p,{children:["create folder ",(0,r.jsx)(n.code,{children:"deploy"})," and create file ",(0,r.jsx)(n.code,{children:"compose.yml"})," inside:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title="deploy/compose.yml"',children:'\nservices:\n traefik:\n image: "traefik:v2.5"\n command:\n - "--api.insecure=true"\n - "--providers.docker=true"\n - "--entrypoints.web.address=:80"\n ports:\n - "80:80"\n volumes:\n - "/var/run/docker.sock:/var/run/docker.sock:ro"\n\n myadmin:\n build: ./myadmin\n restart: always\n env_file:\n - ./myadmin/.env\n volumes:\n - myadmin-db:/code/db\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.myadmin.rule=PathPrefix(`/`)"\n - "traefik.http.services.myadmin.loadbalancer.server.port=3500"\n - "traefik.http.routers.myadmin.priority=2"\n\nvolumes:\n myadmin-db:\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-3---create-a-ssh-keypair",children:"Step 3 - create a SSH keypair"}),"\n",(0,r.jsxs)(n.p,{children:["Make sure you are in ",(0,r.jsx)(n.code,{children:"deploy"})," folder, run next command here:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy"',children:'mkdir .keys && ssh-keygen -f .keys/id_rsa -N ""\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Now it should create ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa"})," and ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa.pub"})," files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually."]}),"\n",(0,r.jsx)(n.h2,{id:"step-4---gitignore-file",children:"Step 4 - .gitignore file"}),"\n",(0,r.jsxs)(n.p,{children:["Create ",(0,r.jsx)(n.code,{children:"deploy/.gitignore"})," file with next content:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:".terraform/\n.keys/\n*.tfstate\n*.tfstate.*\n*.tfvars\ntfplan\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-5---main-terraform-file-maintf",children:"Step 5 - Main terraform file main.tf"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"main.tf"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="main.tf"',children:'\nlocals {\n app_name = ""\n aws_region = "eu-central-1"\n}\n\n\nprovider "aws" {\n region = local.aws_region\n profile = "myaws"\n}\n\ndata "aws_ami" "ubuntu_linux" {\n most_recent = true\n owners = ["amazon"]\n\n filter {\n name = "name"\n values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]\n }\n}\n\ndata "aws_vpc" "default" {\n default = true\n}\n\n\nresource "aws_eip" "eip" {\n vpc = true\n}\nresource "aws_eip_association" "eip_assoc" {\n instance_id = aws_instance.app_instance.id\n allocation_id = aws_eip.eip.id\n}\n\ndata "aws_subnet" "default_subnet" {\n filter {\n name = "vpc-id"\n values = [data.aws_vpc.default.id]\n }\n\n filter {\n name = "default-for-az"\n values = ["true"]\n }\n\n filter {\n name = "availability-zone"\n values = ["${local.aws_region}a"]\n }\n}\n\nresource "aws_security_group" "instance_sg" {\n name = "${local.app_name}-instance-sg"\n vpc_id = data.aws_vpc.default.id\n\n ingress {\n description = "Allow HTTP"\n from_port = 80\n to_port = 80\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n # SSH\n ingress {\n description = "Allow SSH"\n from_port = 22\n to_port = 22\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n egress {\n description = "Allow all outbound traffic"\n from_port = 0\n to_port = 0\n protocol = "-1"\n cidr_blocks = ["0.0.0.0/0"]\n }\n}\n\nresource "aws_key_pair" "app_deployer" {\n key_name = "terraform-deploy_${local.app_name}-key"\n public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key\n}\n\nresource "aws_instance" "app_instance" {\n ami = data.aws_ami.ubuntu_linux.id\n instance_type = "t3a.small"\n subnet_id = data.aws_subnet.default_subnet.id\n vpc_security_group_ids = [aws_security_group.instance_sg.id]\n key_name = aws_key_pair.app_deployer.key_name\n\n root_block_device {\n volume_size = 40 // Size in GB for root partition\n volume_type = "gp2"\n }\n\n user_data = <<-EOF\n #!/bin/bash\n sudo apt-get update\n sudo apt-get install ca-certificates curl\n sudo install -m 0755 -d /etc/apt/keyrings\n sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\n sudo chmod a+r /etc/apt/keyrings/docker.asc\n\n # Add the repository to Apt sources:\n echo \\\n "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\\n $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \\\n sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\n sudo apt-get update\n\n sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\n systemctl start docker\n systemctl enable docker\n usermod -a -G docker ubuntu\n EOF\n\n tags = {\n Name = "${local.app_name}-instance"\n }\n}\n\nresource "null_resource" "sync_files_and_run" {\n # Use rsync to exclude node_modules, .git, db\n provisioner "local-exec" {\n # heredoc syntax\n # remove files that where deleted on the source\n command = <<-EOF\n # -o StrictHostKeyChecking=no\n rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \\\n --delete \\\n --exclude \'node_modules\' \\\n --exclude \'.git\' \\\n --exclude \'.terraform\' \\\n --exclude \'terraform*\' \\\n --exclude \'tfplan\' \\\n --exclude \'.keys\' \\\n --exclude \'.vscode\' \\\n --exclude \'.env\' \\\n --exclude \'db\' \\\n --exclude \'up-human/debug\' \\\n --exclude \'up-human/storage\' \\\n ../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/\n EOF\n }\n\n # Run docker compose after files have been copied\n provisioner "remote-exec" {\n inline = [\n # fail bash specially and intentionally to stop the script on error\n "bash -c \'while ! command -v docker &> /dev/null; do echo \\"Waiting for Docker to be installed...\\"; sleep 1; done\'",\n "bash -c \'while ! docker info &> /dev/null; do echo \\"Waiting for Docker to start...\\"; sleep 1; done\'",\n \n # please note that prune might destroy build cache and make build slower, however it releases disk space\n "docker system prune -f",\n # "docker buildx prune -f --filter \'type!=exec.cachemount\'",\n "cd /home/ubuntu/app/deploy",\n "docker compose -p app -f compose.yml up --build -d"\n ]\n\n connection {\n type = "ssh"\n user = "ubuntu"\n private_key = file("./.keys/id_rsa")\n host = aws_eip_association.eip_assoc.public_ip\n }\n }\n\n # Ensure the resource is triggered every time based on timestamp or file hash\n triggers = {\n always_run = timestamp()\n }\n\n depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]\n}\n\n\noutput "instance_public_ip" {\n value = aws_eip_association.eip_assoc.public_ip\n}\n\n\n######### This scetion is for tf state storage ##############\n\n# S3 bucket for storing Terraform state\nresource "aws_s3_bucket" "terraform_state" {\n bucket = "${local.app_name}-terraform-state"\n}\n\nresource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n status = "Enabled"\n id = "Keep only the latest version of the state file"\n\n noncurrent_version_expiration {\n noncurrent_days = 30\n }\n }\n}\n\nresource "aws_s3_bucket_versioning" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n versioning_configuration {\n status = "Enabled"\n }\n}\n\nresource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = "AES256"\n }\n }\n}\n\n# DynamoDB table for state locking\nresource "aws_dynamodb_table" "terraform_lock" {\n name = "${local.app_name}-terraform-lock-table"\n billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand\n\n hash_key = "LockID" # Primary key for the table\n\n attribute {\n name = "LockID"\n type = "S"\n }\n}\n\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters)"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"step-51---configure-aws-profile",children:"Step 5.1 - Configure AWS Profile"}),"\n",(0,r.jsx)(n.p,{children:"Open or create file ~/.aws/credentials and add (if not already there):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"[myaws]\naws_access_key_id = \naws_secret_access_key = \n"})}),"\n",(0,r.jsx)(n.h3,{id:"step-52---run-deployment",children:"Step 5.2 - Run deployment"}),"\n",(0,r.jsx)(n.p,{children:"To run the deployment first time, you need to run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run deployement:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-6---migrate-state-to-the-cloud",children:"Step 6 - Migrate state to the cloud"}),"\n",(0,r.jsx)(n.p,{children:"First deployment had to create S3 bucket and DynamoDB table for storing Terraform state. Now we need to migrate the state to the cloud."}),"\n",(0,r.jsxs)(n.p,{children:["Add to the end of ",(0,r.jsx)(n.code,{children:"main.tf"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="main.tf"',children:'\n# Configure the backend to use the S3 bucket and DynamoDB table\nterraform {\n backend "s3" {\n bucket = "-terraform-state"\n key = "state.tfstate" # Define a specific path for the state file\n region = "eu-central-1"\n profile = "myaws"\n dynamodb_table = "-terraform-lock-table"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters).\nUnfortunately we can't use variables, HashiCorp thinks it is too dangerous \ud83d\ude25"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init -migrate-state\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run test deployment:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-7---cicd---github-actions",children:"Step 7 - CI/CD - Github Actions"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:".github/workflows/deploy.yml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title=".github/workflows/deploy.yml"',children:'name: Deploy \nrun-name: ${{ github.actor }} builds app \ud83d\ude80\non: [push]\njobs:\n Explore-GitHub-Actions:\n runs-on: ubuntu-latest\n steps:\n - run: echo "\ud83c\udf89 The job was automatically triggered by a ${{ github.event_name }} event."\n - run: echo "\ud83d\udc27 This job is now running on a ${{ runner.os }} server"\n - run: echo "\ud83d\udd0e The name of your branch is ${{ github.ref }}"\n - name: Check out repository code\n uses: actions/checkout@v4\n - name: Set up Terraform\n uses: hashicorp/setup-terraform@v2\n with:\n terraform_version: 1.4.6 \n - run: echo "\ud83d\udca1 The ${{ github.repository }} repository has been cloned to the runner."\n - name: Start building\n env:\n VAULT_AWS_ACCESS_KEY_ID: ${{ secrets.VAULT_AWS_ACCESS_KEY_ID }}\n VAULT_AWS_SECRET_ACCESS_KEY: ${{ secrets.VAULT_AWS_SECRET_ACCESS_KEY }}\n VAULT_SSH_PRIVATE_KEY: ${{ secrets.VAULT_SSH_PRIVATE_KEY }}\n VAULT_SSH_PUBLIC_KEY: ${{ secrets.VAULT_SSH_PUBLIC_KEY }}\n run: |\n /bin/sh -x deploy/deploy.sh\n \n - run: echo "\ud83c\udf4f This job\'s status is ${{ job.status }}."\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-61---create-deploy-script",children:"Step 6.1 - Create deploy script"}),"\n",(0,r.jsxs)(n.p,{children:["Now create file ",(0,r.jsx)(n.code,{children:"deploy/deploy.sh"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy/deploy.sh"',children:'\n# cd to dir of script\ncd "$(dirname "$0")"\n\nmkdir -p ~/.aws ./.keys\n\ncat < ~/.aws/credentials\n[myaws]\naws_access_key_id=$VAULT_AWS_ACCESS_KEY_ID\naws_secret_access_key=$VAULT_AWS_SECRET_ACCESS_KEY\nEOF\n\ncat < ./.keys/id_rsa\n$VAULT_SSH_PRIVATE_KEY\nEOF\n\ncat < ./.keys/id_rsa.pub\n$VAULT_SSH_PUBLIC_KEY\nEOF\n\nchmod 600 ./.keys/id_rsa*\n\n# force Terraform to reinitialize the backend without migrating the state.\nterraform init -reconfigure\nterraform plan -out=tfplan\nterraform apply tfplan\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-62---add-secrets-to-github",children:"Step 6.2 - Add secrets to GitHub"}),"\n",(0,r.jsxs)(n.p,{children:["Go to your GitHub repository, then ",(0,r.jsx)(n.code,{children:"Settings"})," -> ",(0,r.jsx)(n.code,{children:"Secrets"})," -> ",(0,r.jsx)(n.code,{children:"New repository secret"})," and add:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_ACCESS_KEY_ID"})," - your AWS access key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_SECRET_ACCESS_KEY"})," - your AWS secret key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PRIVATE_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa"})," and paste to GitHub secrets"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PUBLIC_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa.pub"})," and paste to GitHub secrets"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now you can push your changes to GitHub and see how it will be deployed automatically."})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>i});var r=t(6540);const s={},a=r.createContext(s);function o(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/9ff66148.139232bb.js b/assets/js/9ff66148.139232bb.js deleted file mode 100644 index e23a224f3..000000000 --- a/assets/js/9ff66148.139232bb.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[7449],{4859:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>p,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var r=t(4848),s=t(8453);const a={slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},o=void 0,i={permalink:"/blog/compose-ec2-deployment-github-actions",source:"@site/blog/2024-11-14-compose-ec2-deployment-ci/index.md",title:"Deploy AdminForth to EC2 with terraform on CI",description:"Here is more advanced snippet to deploy AdminForth to Terraform.",date:"2024-11-14T00:00:00.000Z",tags:[{inline:!1,label:"AWS",permalink:"/blog/tags/aws",description:"Amazon Web Services (AWS) is a cloud computing platform that provides a wide range of services for building and deploying applications."},{inline:!1,label:"Terraform",permalink:"/blog/tags/terraform",description:"Terraform is an open-source infrastructure as code software tool created by HashiCorp that enables users to define and provision data center infrastructure using a declarative configuration language."},{inline:!1,label:"GitHub Actions",permalink:"/blog/tags/github-actions",description:"GitHub Actions is a continuous integration and continuous deployment (CI/CD) service provided by GitHub that allows you to automate your software development workflows."}],readingTime:7.155,hasTruncateMarker:!1,authors:[{name:"Ivan Borshcho",title:"Maintainer of AdminForth",url:"https://github.com/ivictbor",imageURL:"https://avatars.githubusercontent.com/u/1838656?v=4",key:"ivanb"}],frontMatter:{slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},unlisted:!1,nextItem:{title:"Deploy AdminForth to EC2 with terraform (without CI)",permalink:"/blog/compose-ec2-deployment"}},c={authorsImageUrls:[void 0]},l=[{value:"Step 1 - Dockerfile",id:"step-1---dockerfile",level:2},{value:"Step 2 - compose.yml",id:"step-2---composeyml",level:2},{value:"Step 3 - create a SSH keypair",id:"step-3---create-a-ssh-keypair",level:2},{value:"Step 4 - .gitignore file",id:"step-4---gitignore-file",level:2},{value:"Step 5 - Main terraform file main.tf",id:"step-5---main-terraform-file-maintf",level:2},{value:"Step 5.1 - Configure AWS Profile",id:"step-51---configure-aws-profile",level:3},{value:"Step 5.2 - Run deployment",id:"step-52---run-deployment",level:3},{value:"Step 6 - Migrate state to the cloud",id:"step-6---migrate-state-to-the-cloud",level:2},{value:"Step 7 - CI/CD - Github Actions",id:"step-7---cicd---github-actions",level:2},{value:"Step 6.1 - Create deploy script",id:"step-61---create-deploy-script",level:3},{value:"Step 6.2 - Add secrets to GitHub",id:"step-62---add-secrets-to-github",level:3}];function d(e){const n={blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"Here is more advanced snippet to deploy AdminForth to Terraform."}),"\n",(0,r.jsx)(n.p,{children:"Here Terraform state will be stored in the cloud, so you can run this deployment from any machine including stateless CI/CD."}),"\n",(0,r.jsx)(n.p,{children:"We will use GitHub Actions as CI/CD, but you can use any other CI/CD, for example self-hosted free WoodpeckerCI."}),"\n",(0,r.jsxs)(n.p,{children:["Assume you have your AdminForth project in ",(0,r.jsx)(n.code,{children:"myadmin"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"step-1---dockerfile",children:"Step 1 - Dockerfile"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"Dockerfile"})," in ",(0,r.jsx)(n.code,{children:"myadmin"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-Dockerfile",metastring:'title="./myadmin/Dockerfile"',children:'# use the same node version which you used during dev\nFROM node:20-alpine\nWORKDIR /code/\nADD package.json package-lock.json /code/\nRUN npm ci \nADD . /code/\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\nCMD ["npm", "run", "startLive"]\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-2---composeyml",children:"Step 2 - compose.yml"}),"\n",(0,r.jsxs)(n.p,{children:["create folder ",(0,r.jsx)(n.code,{children:"deploy"})," and create file ",(0,r.jsx)(n.code,{children:"compose.yml"})," inside:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title="deploy/compose.yml"',children:'\nservices:\n traefik:\n image: "traefik:v2.5"\n command:\n - "--api.insecure=true"\n - "--providers.docker=true"\n - "--entrypoints.web.address=:80"\n ports:\n - "80:80"\n volumes:\n - "/var/run/docker.sock:/var/run/docker.sock:ro"\n\n myadmin:\n build: ./myadmin\n restart: always\n env_file:\n - ./myadmin/.env\n volumes:\n - myadmin-db:/code/db\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.myadmin.rule=PathPrefix(`/`)"\n - "traefik.http.services.myadmin.loadbalancer.server.port=3500"\n - "traefik.http.routers.myadmin.priority=2"\n\nvolumes:\n myadmin-db:\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-3---create-a-ssh-keypair",children:"Step 3 - create a SSH keypair"}),"\n",(0,r.jsxs)(n.p,{children:["Make sure you are in ",(0,r.jsx)(n.code,{children:"deploy"})," folder, run next command here:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy"',children:'mkdir .keys && ssh-keygen -f .keys/id_rsa -N ""\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Now it should create ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa"})," and ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa.pub"})," files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually."]}),"\n",(0,r.jsx)(n.h2,{id:"step-4---gitignore-file",children:"Step 4 - .gitignore file"}),"\n",(0,r.jsxs)(n.p,{children:["Create ",(0,r.jsx)(n.code,{children:"deploy/.gitignore"})," file with next content:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:".terraform/\n.keys/\n*.tfstate\n*.tfstate.*\n*.tfvars\ntfplan\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-5---main-terraform-file-maintf",children:"Step 5 - Main terraform file main.tf"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"main.tf"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="main.tf"',children:'\nlocals {\n app_name = ""\n aws_region = "eu-central-1"\n}\n\n\nprovider "aws" {\n region = local.aws_region\n profile = "myaws"\n}\n\ndata "aws_ami" "ubuntu_linux" {\n most_recent = true\n owners = ["amazon"]\n\n filter {\n name = "name"\n values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]\n }\n}\n\ndata "aws_vpc" "default" {\n default = true\n}\n\n\nresource "aws_eip" "eip" {\n vpc = true\n}\nresource "aws_eip_association" "eip_assoc" {\n instance_id = aws_instance.app_instance.id\n allocation_id = aws_eip.eip.id\n}\n\ndata "aws_subnet" "default_subnet" {\n filter {\n name = "vpc-id"\n values = [data.aws_vpc.default.id]\n }\n\n filter {\n name = "default-for-az"\n values = ["true"]\n }\n\n filter {\n name = "availability-zone"\n values = ["${local.aws_region}a"]\n }\n}\n\nresource "aws_security_group" "instance_sg" {\n name = "${local.app_name}-instance-sg"\n vpc_id = data.aws_vpc.default.id\n\n ingress {\n description = "Allow HTTP"\n from_port = 80\n to_port = 80\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n # SSH\n ingress {\n description = "Allow SSH"\n from_port = 22\n to_port = 22\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n egress {\n description = "Allow all outbound traffic"\n from_port = 0\n to_port = 0\n protocol = "-1"\n cidr_blocks = ["0.0.0.0/0"]\n }\n}\n\nresource "aws_key_pair" "app_deployer" {\n key_name = "terraform-deploy_${local.app_name}-key"\n public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key\n}\n\nresource "aws_instance" "app_instance" {\n ami = data.aws_ami.ubuntu_linux.id\n instance_type = "t3a.small"\n subnet_id = data.aws_subnet.default_subnet.id\n vpc_security_group_ids = [aws_security_group.instance_sg.id]\n key_name = aws_key_pair.app_deployer.key_name\n\n root_block_device {\n volume_size = 40 // Size in GB for root partition\n volume_type = "gp2"\n }\n\n user_data = <<-EOF\n #!/bin/bash\n sudo apt-get update\n sudo apt-get install ca-certificates curl\n sudo install -m 0755 -d /etc/apt/keyrings\n sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\n sudo chmod a+r /etc/apt/keyrings/docker.asc\n\n # Add the repository to Apt sources:\n echo \\\n "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\\n $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \\\n sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\n sudo apt-get update\n\n sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\n systemctl start docker\n systemctl enable docker\n usermod -a -G docker ubuntu\n EOF\n\n tags = {\n Name = "${local.app_name}-instance"\n }\n}\n\nresource "null_resource" "sync_files_and_run" {\n # Use rsync to exclude node_modules, .git, db\n provisioner "local-exec" {\n # heredoc syntax\n # remove files that where deleted on the source\n command = <<-EOF\n # -o StrictHostKeyChecking=no\n rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \\\n --delete \\\n --exclude \'node_modules\' \\\n --exclude \'.git\' \\\n --exclude \'.terraform\' \\\n --exclude \'terraform*\' \\\n --exclude \'tfplan\' \\\n --exclude \'.keys\' \\\n --exclude \'.vscode\' \\\n --exclude \'.env\' \\\n --exclude \'db\' \\\n --exclude \'up-human/debug\' \\\n --exclude \'up-human/storage\' \\\n ../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/\n EOF\n }\n\n # Run docker compose after files have been copied\n provisioner "remote-exec" {\n inline = [\n # fail bash specially and intentionally to stop the script on error\n "bash -c \'while ! command -v docker &> /dev/null; do echo \\"Waiting for Docker to be installed...\\"; sleep 1; done\'",\n "bash -c \'while ! docker info &> /dev/null; do echo \\"Waiting for Docker to start...\\"; sleep 1; done\'",\n \n # please note that prune might destroy build cache and make build slower, however it releases disk space\n "docker system prune -f",\n # "docker buildx prune -f --filter \'type!=exec.cachemount\'",\n "cd /home/ubuntu/app/deploy",\n "docker compose -p app -f compose.yml up --build -d"\n ]\n\n connection {\n type = "ssh"\n user = "ubuntu"\n private_key = file("./.keys/id_rsa")\n host = aws_eip_association.eip_assoc.public_ip\n }\n }\n\n # Ensure the resource is triggered every time based on timestamp or file hash\n triggers = {\n always_run = timestamp()\n }\n\n depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]\n}\n\n\noutput "instance_public_ip" {\n value = aws_eip_association.eip_assoc.public_ip\n}\n\n\n######### This scetion is for tf state storage ##############\n\n# S3 bucket for storing Terraform state\nresource "aws_s3_bucket" "terraform_state" {\n bucket = "${local.app_name}-terraform-state"\n}\n\nresource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n status = "Enabled"\n id = "Keep only the latest version of the state file"\n\n noncurrent_version_expiration {\n noncurrent_days = 30\n }\n }\n}\n\nresource "aws_s3_bucket_versioning" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n versioning_configuration {\n status = "Enabled"\n }\n}\n\nresource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = "AES256"\n }\n }\n}\n\n# DynamoDB table for state locking\nresource "aws_dynamodb_table" "terraform_lock" {\n name = "${local.app_name}-terraform-lock-table"\n billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand\n\n hash_key = "LockID" # Primary key for the table\n\n attribute {\n name = "LockID"\n type = "S"\n }\n}\n\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters)"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"step-51---configure-aws-profile",children:"Step 5.1 - Configure AWS Profile"}),"\n",(0,r.jsx)(n.p,{children:"Open or create file ~/.aws/credentials and add (if not already there):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"[myaws]\naws_access_key_id = \naws_secret_access_key = \n"})}),"\n",(0,r.jsx)(n.h3,{id:"step-52---run-deployment",children:"Step 5.2 - Run deployment"}),"\n",(0,r.jsx)(n.p,{children:"To run the deployment first time, you need to run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run deployement:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-6---migrate-state-to-the-cloud",children:"Step 6 - Migrate state to the cloud"}),"\n",(0,r.jsx)(n.p,{children:"First deployment had to create S3 bucket and DynamoDB table for storing Terraform state. Now we need to migrate the state to the cloud."}),"\n",(0,r.jsxs)(n.p,{children:["Add to the end of ",(0,r.jsx)(n.code,{children:"main.tf"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="main.tf"',children:'\n# Configure the backend to use the S3 bucket and DynamoDB table\nterraform {\n backend "s3" {\n bucket = "-terraform-state"\n key = "state.tfstate" # Define a specific path for the state file\n region = "eu-central-1"\n profile = "myaws"\n dynamodb_table = "-terraform-lock-table"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters).\nUnfortunately we can't use variables, HashiCorp thinks it is too dangerous \ud83d\ude25"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init -migrate-state\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run test deployment:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-7---cicd---github-actions",children:"Step 7 - CI/CD - Github Actions"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:".github/workflows/deploy.yml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title=".github/workflows/deploy.yml"',children:'name: Deploy \nrun-name: ${{ github.actor }} builds app \ud83d\ude80\non: [push]\njobs:\n Explore-GitHub-Actions:\n runs-on: ubuntu-latest\n steps:\n - run: echo "\ud83c\udf89 The job was automatically triggered by a ${{ github.event_name }} event."\n - run: echo "\ud83d\udc27 This job is now running on a ${{ runner.os }} server"\n - run: echo "\ud83d\udd0e The name of your branch is ${{ github.ref }}"\n - name: Check out repository code\n uses: actions/checkout@v4\n - name: Set up Terraform\n uses: hashicorp/setup-terraform@v2\n with:\n terraform_version: 1.4.6 \n - run: echo "\ud83d\udca1 The ${{ github.repository }} repository has been cloned to the runner."\n - name: Start building\n env:\n VAULT_AWS_ACCESS_KEY_ID: ${{ secrets.VAULT_AWS_ACCESS_KEY_ID }}\n VAULT_AWS_SECRET_ACCESS_KEY: ${{ secrets.VAULT_AWS_SECRET_ACCESS_KEY }}\n VAULT_SSH_PRIVATE_KEY: ${{ secrets.VAULT_SSH_PRIVATE_KEY }}\n VAULT_SSH_PUBLIC_KEY: ${{ secrets.VAULT_SSH_PUBLIC_KEY }}\n run: |\n /bin/sh -x deploy/deploy.sh\n \n - run: echo "\ud83c\udf4f This job\'s status is ${{ job.status }}."\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-61---create-deploy-script",children:"Step 6.1 - Create deploy script"}),"\n",(0,r.jsxs)(n.p,{children:["Now create file ",(0,r.jsx)(n.code,{children:"deploy/deploy.sh"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy/deploy.sh"',children:'\n# cd to dir of script\ncd "$(dirname "$0")"\n\nmkdir -p ~/.aws ./.keys\n\ncat < ~/.aws/credentials\n[myaws]\naws_access_key_id=$VAULT_AWS_ACCESS_KEY_ID\naws_secret_access_key=$VAULT_AWS_SECRET_ACCESS_KEY\nEOF\n\ncat < ./.keys/id_rsa\n$VAULT_SSH_PRIVATE_KEY\nEOF\n\ncat < ./.keys/id_rsa.pub\n$VAULT_SSH_PUBLIC_KEY\nEOF\n\nchmod 600 ./.keys/id_rsa*\n\n# force Terraform to reinitialize the backend without migrating the state.\nterraform init -reconfigure\nterraform plan -out=tfplan\nterraform apply tfplan\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-62---add-secrets-to-github",children:"Step 6.2 - Add secrets to GitHub"}),"\n",(0,r.jsxs)(n.p,{children:["Go to your GitHub repository, then ",(0,r.jsx)(n.code,{children:"Settings"})," -> ",(0,r.jsx)(n.code,{children:"Secrets"})," -> ",(0,r.jsx)(n.code,{children:"New repository secret"})," and add:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_ACCESS_KEY_ID"})," - your AWS access key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_SECRET_ACCESS_KEY"})," - your AWS secret key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PRIVATE_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa"})," and paste to GitHub secrets"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PUBLIC_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa.pub"})," and paste to GitHub secrets"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now you can push your changes to GitHub and see how it will be deployed automatically."})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>o,x:()=>i});var r=t(6540);const s={},a=r.createContext(s);function o(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/9ff66148.f8a1845e.js b/assets/js/9ff66148.f8a1845e.js new file mode 100644 index 000000000..68f30058f --- /dev/null +++ b/assets/js/9ff66148.f8a1845e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[7449],{4859:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>p,frontMatter:()=>o,metadata:()=>i,toc:()=>l});var r=t(4848),s=t(8453);const o={slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},a=void 0,i={permalink:"/blog/compose-ec2-deployment-github-actions",source:"@site/blog/2024-11-14-compose-ec2-deployment-ci/index.md",title:"Deploy AdminForth to EC2 with terraform on CI",description:"Here is more advanced snippet to deploy AdminForth to Terraform.",date:"2024-11-14T00:00:00.000Z",tags:[{inline:!1,label:"AWS",permalink:"/blog/tags/aws",description:"Amazon Web Services (AWS) is a cloud computing platform that provides a wide range of services for building and deploying applications."},{inline:!1,label:"Terraform",permalink:"/blog/tags/terraform",description:"Terraform is an open-source infrastructure as code software tool created by HashiCorp that enables users to define and provision data center infrastructure using a declarative configuration language."},{inline:!1,label:"GitHub Actions",permalink:"/blog/tags/github-actions",description:"GitHub Actions is a continuous integration and continuous deployment (CI/CD) service provided by GitHub that allows you to automate your software development workflows."}],readingTime:7.17,hasTruncateMarker:!1,authors:[{name:"Ivan Borshcho",title:"Maintainer of AdminForth",url:"https://github.com/ivictbor",imageURL:"https://avatars.githubusercontent.com/u/1838656?v=4",key:"ivanb"}],frontMatter:{slug:"compose-ec2-deployment-github-actions",title:"Deploy AdminForth to EC2 with terraform on CI",authors:"ivanb",tags:["aws","terraform","github-actions"]},unlisted:!1,nextItem:{title:"Deploy AdminForth to EC2 with terraform (without CI)",permalink:"/blog/compose-ec2-deployment"}},c={authorsImageUrls:[void 0]},l=[{value:"Step 1 - Dockerfile",id:"step-1---dockerfile",level:2},{value:"Step 2 - compose.yml",id:"step-2---composeyml",level:2},{value:"Step 3 - create a SSH keypair",id:"step-3---create-a-ssh-keypair",level:2},{value:"Step 4 - .gitignore file",id:"step-4---gitignore-file",level:2},{value:"Step 5 - Main terraform file main.tf",id:"step-5---main-terraform-file-maintf",level:2},{value:"Step 5.1 - Configure AWS Profile",id:"step-51---configure-aws-profile",level:3},{value:"Step 5.2 - Run deployment",id:"step-52---run-deployment",level:3},{value:"Step 6 - Migrate state to the cloud",id:"step-6---migrate-state-to-the-cloud",level:2},{value:"Step 7 - CI/CD - Github Actions",id:"step-7---cicd---github-actions",level:2},{value:"Step 7.1 - Create deploy script",id:"step-71---create-deploy-script",level:3},{value:"Step 7.2 - Add secrets to GitHub",id:"step-72---add-secrets-to-github",level:3}];function d(e){const n={blockquote:"blockquote",code:"code",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",ul:"ul",...(0,s.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.p,{children:"Here is more advanced snippet to deploy AdminForth to Terraform."}),"\n",(0,r.jsx)(n.p,{children:"Here Terraform state will be stored in the cloud, so you can run this deployment from any machine including stateless CI/CD."}),"\n",(0,r.jsx)(n.p,{children:"We will use GitHub Actions as CI/CD, but you can use any other CI/CD, for example self-hosted free WoodpeckerCI."}),"\n",(0,r.jsxs)(n.p,{children:["Assume you have your AdminForth project in ",(0,r.jsx)(n.code,{children:"myadmin"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"step-1---dockerfile",children:"Step 1 - Dockerfile"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"Dockerfile"})," in ",(0,r.jsx)(n.code,{children:"myadmin"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-Dockerfile",metastring:'title="./myadmin/Dockerfile"',children:'# use the same node version which you used during dev\nFROM node:20-alpine\nWORKDIR /code/\nADD package.json package-lock.json /code/\nRUN npm ci \nADD . /code/\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\nCMD ["npm", "run", "startLive"]\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-2---composeyml",children:"Step 2 - compose.yml"}),"\n",(0,r.jsxs)(n.p,{children:["create folder ",(0,r.jsx)(n.code,{children:"deploy"})," and create file ",(0,r.jsx)(n.code,{children:"compose.yml"})," inside:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title="deploy/compose.yml"',children:'\nservices:\n traefik:\n image: "traefik:v2.5"\n command:\n - "--api.insecure=true"\n - "--providers.docker=true"\n - "--entrypoints.web.address=:80"\n ports:\n - "80:80"\n volumes:\n - "/var/run/docker.sock:/var/run/docker.sock:ro"\n\n myadmin:\n build: ./myadmin\n restart: always\n env_file:\n - ./myadmin/.env\n volumes:\n - myadmin-db:/code/db\n labels:\n - "traefik.enable=true"\n - "traefik.http.routers.myadmin.rule=PathPrefix(`/`)"\n - "traefik.http.services.myadmin.loadbalancer.server.port=3500"\n - "traefik.http.routers.myadmin.priority=2"\n\nvolumes:\n myadmin-db:\n'})}),"\n",(0,r.jsx)(n.h2,{id:"step-3---create-a-ssh-keypair",children:"Step 3 - create a SSH keypair"}),"\n",(0,r.jsxs)(n.p,{children:["Make sure you are in ",(0,r.jsx)(n.code,{children:"deploy"})," folder, run next command here:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy"',children:'mkdir .keys && ssh-keygen -f .keys/id_rsa -N ""\n'})}),"\n",(0,r.jsxs)(n.p,{children:["Now it should create ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa"})," and ",(0,r.jsx)(n.code,{children:"deploy/.keys/id_rsa.pub"})," files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually."]}),"\n",(0,r.jsx)(n.h2,{id:"step-4---gitignore-file",children:"Step 4 - .gitignore file"}),"\n",(0,r.jsxs)(n.p,{children:["Create ",(0,r.jsx)(n.code,{children:"deploy/.gitignore"})," file with next content:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:".terraform/\n.keys/\n*.tfstate\n*.tfstate.*\n*.tfvars\ntfplan\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-5---main-terraform-file-maintf",children:"Step 5 - Main terraform file main.tf"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:"main.tf"})," in ",(0,r.jsx)(n.code,{children:"deploy"})," folder:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="deploy/main.tf"',children:'\nlocals {\n app_name = ""\n aws_region = "eu-central-1"\n}\n\n\nprovider "aws" {\n region = local.aws_region\n profile = "myaws"\n}\n\ndata "aws_ami" "ubuntu_linux" {\n most_recent = true\n owners = ["amazon"]\n\n filter {\n name = "name"\n values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]\n }\n}\n\ndata "aws_vpc" "default" {\n default = true\n}\n\n\nresource "aws_eip" "eip" {\n vpc = true\n}\nresource "aws_eip_association" "eip_assoc" {\n instance_id = aws_instance.app_instance.id\n allocation_id = aws_eip.eip.id\n}\n\ndata "aws_subnet" "default_subnet" {\n filter {\n name = "vpc-id"\n values = [data.aws_vpc.default.id]\n }\n\n filter {\n name = "default-for-az"\n values = ["true"]\n }\n\n filter {\n name = "availability-zone"\n values = ["${local.aws_region}a"]\n }\n}\n\nresource "aws_security_group" "instance_sg" {\n name = "${local.app_name}-instance-sg"\n vpc_id = data.aws_vpc.default.id\n\n ingress {\n description = "Allow HTTP"\n from_port = 80\n to_port = 80\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n # SSH\n ingress {\n description = "Allow SSH"\n from_port = 22\n to_port = 22\n protocol = "tcp"\n cidr_blocks = ["0.0.0.0/0"]\n }\n\n egress {\n description = "Allow all outbound traffic"\n from_port = 0\n to_port = 0\n protocol = "-1"\n cidr_blocks = ["0.0.0.0/0"]\n }\n}\n\nresource "aws_key_pair" "app_deployer" {\n key_name = "terraform-deploy_${local.app_name}-key"\n public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key\n}\n\nresource "aws_instance" "app_instance" {\n ami = data.aws_ami.ubuntu_linux.id\n instance_type = "t3a.small"\n subnet_id = data.aws_subnet.default_subnet.id\n vpc_security_group_ids = [aws_security_group.instance_sg.id]\n key_name = aws_key_pair.app_deployer.key_name\n\n root_block_device {\n volume_size = 40 // Size in GB for root partition\n volume_type = "gp2"\n }\n\n user_data = <<-EOF\n #!/bin/bash\n sudo apt-get update\n sudo apt-get install ca-certificates curl\n sudo install -m 0755 -d /etc/apt/keyrings\n sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\n sudo chmod a+r /etc/apt/keyrings/docker.asc\n\n # Add the repository to Apt sources:\n echo \\\n "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\\n $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \\\n sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\n sudo apt-get update\n\n sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\n\n systemctl start docker\n systemctl enable docker\n usermod -a -G docker ubuntu\n EOF\n\n tags = {\n Name = "${local.app_name}-instance"\n }\n}\n\nresource "null_resource" "sync_files_and_run" {\n # Use rsync to exclude node_modules, .git, db\n provisioner "local-exec" {\n # heredoc syntax\n # remove files that where deleted on the source\n command = <<-EOF\n # -o StrictHostKeyChecking=no\n rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \\\n --delete \\\n --exclude \'node_modules\' \\\n --exclude \'.git\' \\\n --exclude \'.terraform\' \\\n --exclude \'terraform*\' \\\n --exclude \'tfplan\' \\\n --exclude \'.keys\' \\\n --exclude \'.vscode\' \\\n --exclude \'.env\' \\\n --exclude \'db\' \\\n --exclude \'up-human/debug\' \\\n --exclude \'up-human/storage\' \\\n ../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/\n EOF\n }\n\n # Run docker compose after files have been copied\n provisioner "remote-exec" {\n inline = [\n # fail bash specially and intentionally to stop the script on error\n "bash -c \'while ! command -v docker &> /dev/null; do echo \\"Waiting for Docker to be installed...\\"; sleep 1; done\'",\n "bash -c \'while ! docker info &> /dev/null; do echo \\"Waiting for Docker to start...\\"; sleep 1; done\'",\n \n # please note that prune might destroy build cache and make build slower, however it releases disk space\n "docker system prune -f",\n # "docker buildx prune -f --filter \'type!=exec.cachemount\'",\n "cd /home/ubuntu/app/deploy",\n "docker compose -p app -f compose.yml up --build -d"\n ]\n\n connection {\n type = "ssh"\n user = "ubuntu"\n private_key = file("./.keys/id_rsa")\n host = aws_eip_association.eip_assoc.public_ip\n }\n }\n\n # Ensure the resource is triggered every time based on timestamp or file hash\n triggers = {\n always_run = timestamp()\n }\n\n depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]\n}\n\n\noutput "instance_public_ip" {\n value = aws_eip_association.eip_assoc.public_ip\n}\n\n\n######### This scetion is for tf state storage ##############\n\n# S3 bucket for storing Terraform state\nresource "aws_s3_bucket" "terraform_state" {\n bucket = "${local.app_name}-terraform-state"\n}\n\nresource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n status = "Enabled"\n id = "Keep only the latest version of the state file"\n\n noncurrent_version_expiration {\n noncurrent_days = 30\n }\n }\n}\n\nresource "aws_s3_bucket_versioning" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n versioning_configuration {\n status = "Enabled"\n }\n}\n\nresource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {\n bucket = aws_s3_bucket.terraform_state.bucket\n\n rule {\n apply_server_side_encryption_by_default {\n sse_algorithm = "AES256"\n }\n }\n}\n\n# DynamoDB table for state locking\nresource "aws_dynamodb_table" "terraform_lock" {\n name = "${local.app_name}-terraform-lock-table"\n billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand\n\n hash_key = "LockID" # Primary key for the table\n\n attribute {\n name = "LockID"\n type = "S"\n }\n}\n\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters)"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"step-51---configure-aws-profile",children:"Step 5.1 - Configure AWS Profile"}),"\n",(0,r.jsx)(n.p,{children:"Open or create file ~/.aws/credentials and add (if not already there):"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:"[myaws]\naws_access_key_id = \naws_secret_access_key = \n"})}),"\n",(0,r.jsx)(n.h3,{id:"step-52---run-deployment",children:"Step 5.2 - Run deployment"}),"\n",(0,r.jsx)(n.p,{children:"To run the deployment first time, you need to run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run deployement:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-6---migrate-state-to-the-cloud",children:"Step 6 - Migrate state to the cloud"}),"\n",(0,r.jsx)(n.p,{children:"First deployment had to create S3 bucket and DynamoDB table for storing Terraform state. Now we need to migrate the state to the cloud."}),"\n",(0,r.jsxs)(n.p,{children:["Add to the end of ",(0,r.jsx)(n.code,{children:"main.tf"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-hcl",metastring:'title="main.tf"',children:'\n# Configure the backend to use the S3 bucket and DynamoDB table\nterraform {\n backend "s3" {\n bucket = "-terraform-state"\n key = "state.tfstate" # Define a specific path for the state file\n region = "eu-central-1"\n profile = "myaws"\n dynamodb_table = "-terraform-lock-table"\n }\n}\n'})}),"\n",(0,r.jsxs)(n.blockquote,{children:["\n",(0,r.jsxs)(n.p,{children:["\ud83d\udc46 Replace ",(0,r.jsx)(n.code,{children:""})," with your app name (no spaces, only underscores or letters).\nUnfortunately we can't use variables, HashiCorp thinks it is too dangerous \ud83d\ude25"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now run:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform init -migrate-state\n"})}),"\n",(0,r.jsx)(n.p,{children:"Now run test deployment:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"terraform apply -auto-approve\n"})}),"\n",(0,r.jsx)(n.h2,{id:"step-7---cicd---github-actions",children:"Step 7 - CI/CD - Github Actions"}),"\n",(0,r.jsxs)(n.p,{children:["Create file ",(0,r.jsx)(n.code,{children:".github/workflows/deploy.yml"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yml",metastring:'title=".github/workflows/deploy.yml"',children:'name: Deploy \nrun-name: ${{ github.actor }} builds app \ud83d\ude80\non: [push]\njobs:\n Explore-GitHub-Actions:\n runs-on: ubuntu-latest\n steps:\n - run: echo "\ud83c\udf89 The job was automatically triggered by a ${{ github.event_name }} event."\n - run: echo "\ud83d\udc27 This job is now running on a ${{ runner.os }} server"\n - run: echo "\ud83d\udd0e The name of your branch is ${{ github.ref }}"\n - name: Check out repository code\n uses: actions/checkout@v4\n - name: Set up Terraform\n uses: hashicorp/setup-terraform@v2\n with:\n terraform_version: 1.4.6 \n - run: echo "\ud83d\udca1 The ${{ github.repository }} repository has been cloned to the runner."\n - name: Start building\n env:\n VAULT_AWS_ACCESS_KEY_ID: ${{ secrets.VAULT_AWS_ACCESS_KEY_ID }}\n VAULT_AWS_SECRET_ACCESS_KEY: ${{ secrets.VAULT_AWS_SECRET_ACCESS_KEY }}\n VAULT_SSH_PRIVATE_KEY: ${{ secrets.VAULT_SSH_PRIVATE_KEY }}\n VAULT_SSH_PUBLIC_KEY: ${{ secrets.VAULT_SSH_PUBLIC_KEY }}\n run: |\n /bin/sh -x deploy/deploy.sh\n \n - run: echo "\ud83c\udf4f This job\'s status is ${{ job.status }}."\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-71---create-deploy-script",children:"Step 7.1 - Create deploy script"}),"\n",(0,r.jsxs)(n.p,{children:["Now create file ",(0,r.jsx)(n.code,{children:"deploy/deploy.sh"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",metastring:'title="deploy/deploy.sh"',children:'\n# cd to dir of script\ncd "$(dirname "$0")"\n\nmkdir -p ~/.aws ./.keys\n\ncat < ~/.aws/credentials\n[myaws]\naws_access_key_id=$VAULT_AWS_ACCESS_KEY_ID\naws_secret_access_key=$VAULT_AWS_SECRET_ACCESS_KEY\nEOF\n\ncat < ./.keys/id_rsa\n$VAULT_SSH_PRIVATE_KEY\nEOF\n\ncat < ./.keys/id_rsa.pub\n$VAULT_SSH_PUBLIC_KEY\nEOF\n\nchmod 600 ./.keys/id_rsa*\n\n# force Terraform to reinitialize the backend without migrating the state.\nterraform init -reconfigure\nterraform plan -out=tfplan\nterraform apply tfplan\n'})}),"\n",(0,r.jsx)(n.h3,{id:"step-72---add-secrets-to-github",children:"Step 7.2 - Add secrets to GitHub"}),"\n",(0,r.jsxs)(n.p,{children:["Go to your GitHub repository, then ",(0,r.jsx)(n.code,{children:"Settings"})," -> ",(0,r.jsx)(n.code,{children:"Secrets"})," -> ",(0,r.jsx)(n.code,{children:"New repository secret"})," and add:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_ACCESS_KEY_ID"})," - your AWS access key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_AWS_SECRET_ACCESS_KEY"})," - your AWS secret key"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PRIVATE_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa"})," and paste to GitHub secrets"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.code,{children:"VAULT_SSH_PUBLIC_KEY"})," - make ",(0,r.jsx)(n.code,{children:"cat ~/.ssh/id_rsa.pub"})," and paste to GitHub secrets"]}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"Now you can push your changes to GitHub and see how it will be deployed automatically."})]})}function p(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>i});var r=t(6540);const s={},o=r.createContext(s);function a(e){const n=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),r.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/b3491139.f5bc8a00.js b/assets/js/b3491139.f5bc8a00.js new file mode 100644 index 000000000..cb8f82249 --- /dev/null +++ b/assets/js/b3491139.f5bc8a00.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[2641],{7388:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>r,contentTitle:()=>a,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var l=t(4848),s=t(8453);const o={},a="AdminForth Components Library",i={id:"tutorial/Customization/afcl",title:"AdminForth Components Library",description:"ACL is a new set of components which you can use as build blocks.",source:"@site/docs/tutorial/03-Customization/14-afcl.md",sourceDirName:"tutorial/03-Customization",slug:"/tutorial/Customization/afcl",permalink:"/docs/tutorial/Customization/afcl",draft:!1,unlisted:!1,tags:[],version:"current",sidebarPosition:14,frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Standard pages tuning",permalink:"/docs/tutorial/Customization/standardPagesTuning"},next:{title:"Deploy in Docker",permalink:"/docs/tutorial/deploy"}},r={},c=[{value:"Button",id:"button",level:2},{value:"Link",id:"link",level:2},{value:"Select",id:"select",level:2},{value:"Single",id:"single",level:3},{value:"Multiple",id:"multiple",level:3},{value:"Custom slots for item",id:"custom-slots-for-item",level:3}];function d(e){const n={code:"code",h1:"h1",h2:"h2",h3:"h3",img:"img",p:"p",pre:"pre",...(0,s.R)(),...e.components};return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(n.h1,{id:"adminforth-components-library",children:"AdminForth Components Library"}),"\n",(0,l.jsx)(n.p,{children:"ACL is a new set of components which you can use as build blocks.\nThis allows to keep the design consistent with minimal efforts. ACL components will follow styling standard and respect theme colors."}),"\n",(0,l.jsx)(n.h2,{id:"button",children:"Button"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-js",children:"import { Button } from '@/afcl'\n"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-vue",children:'\n'})}),"\n",(0,l.jsx)(n.p,{children:"loader prop would show loader when it's true."}),"\n",(0,l.jsx)(n.h2,{id:"link",children:"Link"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-js",children:"import { Link } from '@/afcl'\n"})}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-vue",children:'Go to login\n'})}),"\n",(0,l.jsx)(n.h2,{id:"select",children:"Select"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-js",children:"import { Select } from '@/afcl'\n\n\nconst selected = ref(null)\n"})}),"\n",(0,l.jsx)(n.h3,{id:"single",children:"Single"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-vue",children:"\n"})}),"\n",(0,l.jsx)(n.h3,{id:"multiple",children:"Multiple"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-vue",children:"\n"})}),"\n",(0,l.jsx)(n.h3,{id:"custom-slots-for-item",children:"Custom slots for item"}),"\n",(0,l.jsx)(n.pre,{children:(0,l.jsx)(n.code,{className:"language-vue",children:"\n \n\n \n \n\n"})}),"\n",(0,l.jsx)(n.p,{children:(0,l.jsx)(n.img,{alt:"alt text",src:t(7389).A+"",width:"774",height:"659"})})]})}function u(e={}){const{wrapper:n}={...(0,s.R)(),...e.components};return n?(0,l.jsx)(n,{...e,children:(0,l.jsx)(d,{...e})}):d(e)}},7389:(e,n,t)=>{t.d(n,{A:()=>l});const l=t.p+"assets/images/image-16-6cbe8efea058e3cf92a2e2a9e776c6fa.png"},8453:(e,n,t)=>{t.d(n,{R:()=>a,x:()=>i});var l=t(6540);const s={},o=l.createContext(s);function a(e){const n=l.useContext(o);return l.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),l.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/assets/js/dbfc4782.8abce623.js b/assets/js/dbfc4782.8abce623.js deleted file mode 100644 index 770b7feb3..000000000 --- a/assets/js/dbfc4782.8abce623.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkadminforth=self.webpackChunkadminforth||[]).push([[8749],{1895:n=>{n.exports=JSON.parse('{"archive":{"blogPosts":[{"id":"compose-ec2-deployment-github-actions","metadata":{"permalink":"/blog/compose-ec2-deployment-github-actions","source":"@site/blog/2024-11-14-compose-ec2-deployment-ci/index.md","title":"Deploy AdminForth to EC2 with terraform on CI","description":"Here is more advanced snippet to deploy AdminForth to Terraform.","date":"2024-11-14T00:00:00.000Z","tags":[{"inline":false,"label":"AWS","permalink":"/blog/tags/aws","description":"Amazon Web Services (AWS) is a cloud computing platform that provides a wide range of services for building and deploying applications."},{"inline":false,"label":"Terraform","permalink":"/blog/tags/terraform","description":"Terraform is an open-source infrastructure as code software tool created by HashiCorp that enables users to define and provision data center infrastructure using a declarative configuration language."},{"inline":false,"label":"GitHub Actions","permalink":"/blog/tags/github-actions","description":"GitHub Actions is a continuous integration and continuous deployment (CI/CD) service provided by GitHub that allows you to automate your software development workflows."}],"readingTime":7.155,"hasTruncateMarker":false,"authors":[{"name":"Ivan Borshcho","title":"Maintainer of AdminForth","url":"https://github.com/ivictbor","imageURL":"https://avatars.githubusercontent.com/u/1838656?v=4","key":"ivanb"}],"frontMatter":{"slug":"compose-ec2-deployment-github-actions","title":"Deploy AdminForth to EC2 with terraform on CI","authors":"ivanb","tags":["aws","terraform","github-actions"]},"unlisted":false,"nextItem":{"title":"Deploy AdminForth to EC2 with terraform (without CI)","permalink":"/blog/compose-ec2-deployment"}},"content":"Here is more advanced snippet to deploy AdminForth to Terraform.\\n\\nHere Terraform state will be stored in the cloud, so you can run this deployment from any machine including stateless CI/CD.\\n\\nWe will use GitHub Actions as CI/CD, but you can use any other CI/CD, for example self-hosted free WoodpeckerCI.\\n\\nAssume you have your AdminForth project in `myadmin`.\\n\\n\\n## Step 1 - Dockerfile\\n\\nCreate file `Dockerfile` in `myadmin`:\\n\\n```Dockerfile title=\\"./myadmin/Dockerfile\\"\\n# use the same node version which you used during dev\\nFROM node:20-alpine\\nWORKDIR /code/\\nADD package.json package-lock.json /code/\\nRUN npm ci \\nADD . /code/\\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\\nCMD [\\"npm\\", \\"run\\", \\"startLive\\"]\\n```\\n\\n\\n## Step 2 - compose.yml\\n\\ncreate folder `deploy` and create file `compose.yml` inside:\\n\\n```yml title=\\"deploy/compose.yml\\"\\n\\nservices:\\n traefik:\\n image: \\"traefik:v2.5\\"\\n command:\\n - \\"--api.insecure=true\\"\\n - \\"--providers.docker=true\\"\\n - \\"--entrypoints.web.address=:80\\"\\n ports:\\n - \\"80:80\\"\\n volumes:\\n - \\"/var/run/docker.sock:/var/run/docker.sock:ro\\"\\n\\n myadmin:\\n build: ./myadmin\\n restart: always\\n env_file:\\n - ./myadmin/.env\\n volumes:\\n - myadmin-db:/code/db\\n labels:\\n - \\"traefik.enable=true\\"\\n - \\"traefik.http.routers.myadmin.rule=PathPrefix(`/`)\\"\\n - \\"traefik.http.services.myadmin.loadbalancer.server.port=3500\\"\\n - \\"traefik.http.routers.myadmin.priority=2\\"\\n\\nvolumes:\\n myadmin-db:\\n```\\n\\n## Step 3 - create a SSH keypair\\n\\nMake sure you are in `deploy` folder, run next command here:\\n\\n```bash title=\\"deploy\\"\\nmkdir .keys && ssh-keygen -f .keys/id_rsa -N \\"\\"\\n```\\n\\nNow it should create `deploy/.keys/id_rsa` and `deploy/.keys/id_rsa.pub` files with your SSH keypair. Terraform script will put the public key to the EC2 instance and will use private key to connect to the instance. Also you will be able to use it to connect to the instance manually.\\n\\n## Step 4 - .gitignore file\\n\\nCreate `deploy/.gitignore` file with next content:\\n\\n```bash\\n.terraform/\\n.keys/\\n*.tfstate\\n*.tfstate.*\\n*.tfvars\\ntfplan\\n```\\n\\n## Step 5 - Main terraform file main.tf\\n\\n\\nCreate file `main.tf`:\\n\\n```hcl title=\\"main.tf\\"\\n\\nlocals {\\n app_name = \\"\\"\\n aws_region = \\"eu-central-1\\"\\n}\\n\\n\\nprovider \\"aws\\" {\\n region = local.aws_region\\n profile = \\"myaws\\"\\n}\\n\\ndata \\"aws_ami\\" \\"ubuntu_linux\\" {\\n most_recent = true\\n owners = [\\"amazon\\"]\\n\\n filter {\\n name = \\"name\\"\\n values = [\\"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\\"]\\n }\\n}\\n\\ndata \\"aws_vpc\\" \\"default\\" {\\n default = true\\n}\\n\\n\\nresource \\"aws_eip\\" \\"eip\\" {\\n vpc = true\\n}\\nresource \\"aws_eip_association\\" \\"eip_assoc\\" {\\n instance_id = aws_instance.app_instance.id\\n allocation_id = aws_eip.eip.id\\n}\\n\\ndata \\"aws_subnet\\" \\"default_subnet\\" {\\n filter {\\n name = \\"vpc-id\\"\\n values = [data.aws_vpc.default.id]\\n }\\n\\n filter {\\n name = \\"default-for-az\\"\\n values = [\\"true\\"]\\n }\\n\\n filter {\\n name = \\"availability-zone\\"\\n values = [\\"${local.aws_region}a\\"]\\n }\\n}\\n\\nresource \\"aws_security_group\\" \\"instance_sg\\" {\\n name = \\"${local.app_name}-instance-sg\\"\\n vpc_id = data.aws_vpc.default.id\\n\\n ingress {\\n description = \\"Allow HTTP\\"\\n from_port = 80\\n to_port = 80\\n protocol = \\"tcp\\"\\n cidr_blocks = [\\"0.0.0.0/0\\"]\\n }\\n\\n # SSH\\n ingress {\\n description = \\"Allow SSH\\"\\n from_port = 22\\n to_port = 22\\n protocol = \\"tcp\\"\\n cidr_blocks = [\\"0.0.0.0/0\\"]\\n }\\n\\n egress {\\n description = \\"Allow all outbound traffic\\"\\n from_port = 0\\n to_port = 0\\n protocol = \\"-1\\"\\n cidr_blocks = [\\"0.0.0.0/0\\"]\\n }\\n}\\n\\nresource \\"aws_key_pair\\" \\"app_deployer\\" {\\n key_name = \\"terraform-deploy_${local.app_name}-key\\"\\n public_key = file(\\"./.keys/id_rsa.pub\\") # Path to your public SSH key\\n}\\n\\nresource \\"aws_instance\\" \\"app_instance\\" {\\n ami = data.aws_ami.ubuntu_linux.id\\n instance_type = \\"t3a.small\\"\\n subnet_id = data.aws_subnet.default_subnet.id\\n vpc_security_group_ids = [aws_security_group.instance_sg.id]\\n key_name = aws_key_pair.app_deployer.key_name\\n\\n root_block_device {\\n volume_size = 40 // Size in GB for root partition\\n volume_type = \\"gp2\\"\\n }\\n\\n user_data = <<-EOF\\n #!/bin/bash\\n sudo apt-get update\\n sudo apt-get install ca-certificates curl\\n sudo install -m 0755 -d /etc/apt/keyrings\\n sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\\n sudo chmod a+r /etc/apt/keyrings/docker.asc\\n\\n # Add the repository to Apt sources:\\n echo \\\\\\n \\"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\\\\\n $(. /etc/os-release && echo \\"$VERSION_CODENAME\\") stable\\" | \\\\\\n sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\\n sudo apt-get update\\n\\n sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\\n\\n systemctl start docker\\n systemctl enable docker\\n usermod -a -G docker ubuntu\\n EOF\\n\\n tags = {\\n Name = \\"${local.app_name}-instance\\"\\n }\\n}\\n\\nresource \\"null_resource\\" \\"sync_files_and_run\\" {\\n # Use rsync to exclude node_modules, .git, db\\n provisioner \\"local-exec\\" {\\n # heredoc syntax\\n # remove files that where deleted on the source\\n command = <<-EOF\\n # -o StrictHostKeyChecking=no\\n rsync -t -av -e \\"ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no\\" \\\\\\n --delete \\\\\\n --exclude \'node_modules\' \\\\\\n --exclude \'.git\' \\\\\\n --exclude \'.terraform\' \\\\\\n --exclude \'terraform*\' \\\\\\n --exclude \'tfplan\' \\\\\\n --exclude \'.keys\' \\\\\\n --exclude \'.vscode\' \\\\\\n --exclude \'.env\' \\\\\\n --exclude \'db\' \\\\\\n --exclude \'up-human/debug\' \\\\\\n --exclude \'up-human/storage\' \\\\\\n ../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/\\n EOF\\n }\\n\\n # Run docker compose after files have been copied\\n provisioner \\"remote-exec\\" {\\n inline = [\\n # fail bash specially and intentionally to stop the script on error\\n \\"bash -c \'while ! command -v docker &> /dev/null; do echo \\\\\\"Waiting for Docker to be installed...\\\\\\"; sleep 1; done\'\\",\\n \\"bash -c \'while ! docker info &> /dev/null; do echo \\\\\\"Waiting for Docker to start...\\\\\\"; sleep 1; done\'\\",\\n \\n # please note that prune might destroy build cache and make build slower, however it releases disk space\\n \\"docker system prune -f\\",\\n # \\"docker buildx prune -f --filter \'type!=exec.cachemount\'\\",\\n \\"cd /home/ubuntu/app/deploy\\",\\n \\"docker compose -p app -f compose.yml up --build -d\\"\\n ]\\n\\n connection {\\n type = \\"ssh\\"\\n user = \\"ubuntu\\"\\n private_key = file(\\"./.keys/id_rsa\\")\\n host = aws_eip_association.eip_assoc.public_ip\\n }\\n }\\n\\n # Ensure the resource is triggered every time based on timestamp or file hash\\n triggers = {\\n always_run = timestamp()\\n }\\n\\n depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]\\n}\\n\\n\\noutput \\"instance_public_ip\\" {\\n value = aws_eip_association.eip_assoc.public_ip\\n}\\n\\n\\n######### This scetion is for tf state storage ##############\\n\\n# S3 bucket for storing Terraform state\\nresource \\"aws_s3_bucket\\" \\"terraform_state\\" {\\n bucket = \\"${local.app_name}-terraform-state\\"\\n}\\n\\nresource \\"aws_s3_bucket_lifecycle_configuration\\" \\"terraform_state\\" {\\n bucket = aws_s3_bucket.terraform_state.bucket\\n\\n rule {\\n status = \\"Enabled\\"\\n id = \\"Keep only the latest version of the state file\\"\\n\\n noncurrent_version_expiration {\\n noncurrent_days = 30\\n }\\n }\\n}\\n\\nresource \\"aws_s3_bucket_versioning\\" \\"terraform_state\\" {\\n bucket = aws_s3_bucket.terraform_state.bucket\\n\\n versioning_configuration {\\n status = \\"Enabled\\"\\n }\\n}\\n\\nresource \\"aws_s3_bucket_server_side_encryption_configuration\\" \\"terraform_state\\" {\\n bucket = aws_s3_bucket.terraform_state.bucket\\n\\n rule {\\n apply_server_side_encryption_by_default {\\n sse_algorithm = \\"AES256\\"\\n }\\n }\\n}\\n\\n# DynamoDB table for state locking\\nresource \\"aws_dynamodb_table\\" \\"terraform_lock\\" {\\n name = \\"${local.app_name}-terraform-lock-table\\"\\n billing_mode = \\"PAY_PER_REQUEST\\" # Dynamically scales to meet demand\\n\\n hash_key = \\"LockID\\" # Primary key for the table\\n\\n attribute {\\n name = \\"LockID\\"\\n type = \\"S\\"\\n }\\n}\\n\\n```\\n\\n> \ud83d\udc46 Replace `` with your app name (no spaces, only underscores or letters)\\n\\n### Step 5.1 - Configure AWS Profile\\n\\nOpen or create file ~/.aws/credentials and add (if not already there):\\n\\n```ini\\n[myaws]\\naws_access_key_id = \\naws_secret_access_key = \\n```\\n\\n### Step 5.2 - Run deployment\\n\\nTo run the deployment first time, you need to run:\\n\\n```bash\\nterraform init\\n```\\n\\nNow run deployement:\\n\\n```bash\\nterraform apply -auto-approve\\n```\\n\\n## Step 6 - Migrate state to the cloud\\n\\nFirst deployment had to create S3 bucket and DynamoDB table for storing Terraform state. Now we need to migrate the state to the cloud.\\n\\nAdd to the end of `main.tf`:\\n\\n```hcl title=\\"main.tf\\"\\n\\n# Configure the backend to use the S3 bucket and DynamoDB table\\nterraform {\\n backend \\"s3\\" {\\n bucket = \\"-terraform-state\\"\\n key = \\"state.tfstate\\" # Define a specific path for the state file\\n region = \\"eu-central-1\\"\\n profile = \\"myaws\\"\\n dynamodb_table = \\"-terraform-lock-table\\"\\n }\\n}\\n```\\n\\n> \ud83d\udc46 Replace `` with your app name (no spaces, only underscores or letters). \\n> Unfortunately we can\'t use variables, HashiCorp thinks it is too dangerous \ud83d\ude25\\n\\n\\nNow run:\\n\\n```bash\\nterraform init -migrate-state\\n```\\n\\nNow run test deployment:\\n\\n```bash\\nterraform apply -auto-approve\\n```\\n\\n## Step 7 - CI/CD - Github Actions\\n\\nCreate file `.github/workflows/deploy.yml`:\\n\\n```yml title=\\".github/workflows/deploy.yml\\"\\nname: Deploy \\nrun-name: ${{ github.actor }} builds app \ud83d\ude80\\non: [push]\\njobs:\\n Explore-GitHub-Actions:\\n runs-on: ubuntu-latest\\n steps:\\n - run: echo \\"\ud83c\udf89 The job was automatically triggered by a ${{ github.event_name }} event.\\"\\n - run: echo \\"\ud83d\udc27 This job is now running on a ${{ runner.os }} server\\"\\n - run: echo \\"\ud83d\udd0e The name of your branch is ${{ github.ref }}\\"\\n - name: Check out repository code\\n uses: actions/checkout@v4\\n - name: Set up Terraform\\n uses: hashicorp/setup-terraform@v2\\n with:\\n terraform_version: 1.4.6 \\n - run: echo \\"\ud83d\udca1 The ${{ github.repository }} repository has been cloned to the runner.\\"\\n - name: Start building\\n env:\\n VAULT_AWS_ACCESS_KEY_ID: ${{ secrets.VAULT_AWS_ACCESS_KEY_ID }}\\n VAULT_AWS_SECRET_ACCESS_KEY: ${{ secrets.VAULT_AWS_SECRET_ACCESS_KEY }}\\n VAULT_SSH_PRIVATE_KEY: ${{ secrets.VAULT_SSH_PRIVATE_KEY }}\\n VAULT_SSH_PUBLIC_KEY: ${{ secrets.VAULT_SSH_PUBLIC_KEY }}\\n run: |\\n /bin/sh -x deploy/deploy.sh\\n \\n - run: echo \\"\ud83c\udf4f This job\'s status is ${{ job.status }}.\\"\\n```\\n\\n### Step 6.1 - Create deploy script\\n\\nNow create file `deploy/deploy.sh`:\\n\\n```bash title=\\"deploy/deploy.sh\\"\\n\\n# cd to dir of script\\ncd \\"$(dirname \\"$0\\")\\"\\n\\nmkdir -p ~/.aws ./.keys\\n\\ncat < ~/.aws/credentials\\n[myaws]\\naws_access_key_id=$VAULT_AWS_ACCESS_KEY_ID\\naws_secret_access_key=$VAULT_AWS_SECRET_ACCESS_KEY\\nEOF\\n\\ncat < ./.keys/id_rsa\\n$VAULT_SSH_PRIVATE_KEY\\nEOF\\n\\ncat < ./.keys/id_rsa.pub\\n$VAULT_SSH_PUBLIC_KEY\\nEOF\\n\\nchmod 600 ./.keys/id_rsa*\\n\\n# force Terraform to reinitialize the backend without migrating the state.\\nterraform init -reconfigure\\nterraform plan -out=tfplan\\nterraform apply tfplan\\n```\\n\\n### Step 6.2 - Add secrets to GitHub\\n\\nGo to your GitHub repository, then `Settings` -> `Secrets` -> `New repository secret` and add:\\n\\n- `VAULT_AWS_ACCESS_KEY_ID` - your AWS access key\\n- `VAULT_AWS_SECRET_ACCESS_KEY` - your AWS secret key\\n- `VAULT_SSH_PRIVATE_KEY` - make `cat ~/.ssh/id_rsa` and paste to GitHub secrets\\n- `VAULT_SSH_PUBLIC_KEY` - make `cat ~/.ssh/id_rsa.pub` and paste to GitHub secrets\\n\\n\\nNow you can push your changes to GitHub and see how it will be deployed automatically."},{"id":"compose-ec2-deployment","metadata":{"permalink":"/blog/compose-ec2-deployment","source":"@site/blog/2024-10-31-compose-ec2-deployment/index.md","title":"Deploy AdminForth to EC2 with terraform (without CI)","description":"Here is a row snippet to deploy AdminForth to Terraform.","date":"2024-10-31T00:00:00.000Z","tags":[{"inline":false,"label":"AWS","permalink":"/blog/tags/aws","description":"Amazon Web Services (AWS) is a cloud computing platform that provides a wide range of services for building and deploying applications."},{"inline":false,"label":"Terraform","permalink":"/blog/tags/terraform","description":"Terraform is an open-source infrastructure as code software tool created by HashiCorp that enables users to define and provision data center infrastructure using a declarative configuration language."}],"readingTime":3.04,"hasTruncateMarker":false,"authors":[{"name":"Ivan Borshcho","title":"Maintainer of AdminForth","url":"https://github.com/ivictbor","imageURL":"https://avatars.githubusercontent.com/u/1838656?v=4","key":"ivanb"}],"frontMatter":{"slug":"compose-ec2-deployment","title":"Deploy AdminForth to EC2 with terraform (without CI)","authors":"ivanb","tags":["aws","terraform"]},"unlisted":false,"prevItem":{"title":"Deploy AdminForth to EC2 with terraform on CI","permalink":"/blog/compose-ec2-deployment-github-actions"},"nextItem":{"title":"Build AI-Assisted blog with AdminForth and Nuxt in 20 minutes","permalink":"/blog/ai-blog"}},"content":"Here is a row snippet to deploy AdminForth to Terraform.\\n\\nAssume you have your AdminForth project in `myadmin`.\\n\\nCreate file `Dockerfile` in `myadmin`:\\n\\n```Dockerfile title=\\"./myadmin/Dockerfile\\"\\n# use the same node version which you used during dev\\nFROM node:20-alpine\\nWORKDIR /code/\\nADD package.json package-lock.json /code/\\nRUN npm ci \\nADD . /code/\\nRUN --mount=type=cache,target=/tmp npx tsx bundleNow.ts\\nCMD [\\"npm\\", \\"run\\", \\"startLive\\"]\\n```\\n\\n\\nCreate file `compose.yml`:\\n\\n```yml title=\\"compose.yml\\"\\n\\nservices:\\n traefik:\\n image: \\"traefik:v2.5\\"\\n command:\\n - \\"--api.insecure=true\\"\\n - \\"--providers.docker=true\\"\\n - \\"--entrypoints.web.address=:80\\"\\n ports:\\n - \\"80:80\\"\\n volumes:\\n - \\"/var/run/docker.sock:/var/run/docker.sock:ro\\"\\n\\n myadmin:\\n build: ./myadmin\\n restart: always\\n env_file:\\n - ./myadmin/.env\\n volumes:\\n - myadmin-db:/code/db\\n labels:\\n - \\"traefik.enable=true\\"\\n - \\"traefik.http.routers.myadmin.rule=PathPrefix(`/`)\\"\\n - \\"traefik.http.services.myadmin.loadbalancer.server.port=3500\\"\\n - \\"traefik.http.routers.myadmin.priority=2\\"\\n\\nvolumes:\\n myadmin-db:\\n```\\n\\n\\n\\nCreate file `main.tf`:\\n\\n```hcl title=\\"main.tf\\"\\n\\nprovider \\"aws\\" {\\n region = \\"eu-central-1\\"\\n profile = \\"myaws\\"\\n}\\n\\ndata \\"aws_ami\\" \\"ubuntu_linux\\" {\\n most_recent = true\\n owners = [\\"amazon\\"]\\n\\n filter {\\n name = \\"name\\"\\n values = [\\"ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*\\"]\\n }\\n}\\n\\ndata \\"aws_vpc\\" \\"default\\" {\\n default = true\\n}\\n\\nresource \\"aws_eip\\" \\"eip\\" {\\n vpc = true\\n}\\nresource \\"aws_eip_association\\" \\"eip_assoc\\" {\\n instance_id = aws_instance.myadmin_instance.id\\n allocation_id = aws_eip.eip.id\\n}\\n\\ndata \\"aws_subnet\\" \\"default_subnet\\" {\\n filter {\\n name = \\"vpc-id\\"\\n values = [data.aws_vpc.default.id]\\n }\\n\\n filter {\\n name = \\"default-for-az\\"\\n values = [\\"true\\"]\\n }\\n\\n filter {\\n name = \\"availability-zone\\"\\n values = [\\"eu-central-1a\\"]\\n }\\n}\\n\\n\\nresource \\"aws_security_group\\" \\"instance_sg\\" {\\n name = \\"myadmin-instance-sg\\"\\n vpc_id = data.aws_vpc.default.id\\n\\n ingress {\\n description = \\"Allow HTTP\\"\\n from_port = 80\\n to_port = 80\\n protocol = \\"tcp\\"\\n cidr_blocks = [\\"0.0.0.0/0\\"]\\n }\\n\\n # SSH\\n ingress {\\n description = \\"Allow SSH\\"\\n from_port = 22\\n to_port = 22\\n protocol = \\"tcp\\"\\n cidr_blocks = [\\"0.0.0.0/0\\"]\\n }\\n\\n egress {\\n description = \\"Allow all outbound traffic\\"\\n from_port = 0\\n to_port = 0\\n protocol = \\"-1\\"\\n cidr_blocks = [\\"0.0.0.0/0\\"]\\n }\\n}\\n\\nresource \\"aws_key_pair\\" \\"myadmin_deploy_key\\" {\\n key_name = \\"terraform-myadmin_deploy_key-key\\"\\n public_key = file(\\"~/.ssh/id_rsa.pub\\") # Path to your public SSH key\\n}\\n\\n\\nresource \\"aws_instance\\" \\"myadmin_instance\\" {\\n ami = data.aws_ami.ubuntu_linux.id\\n instance_type = \\"t3a.small\\"\\n subnet_id = data.aws_subnet.default_subnet.id\\n vpc_security_group_ids = [aws_security_group.instance_sg.id]\\n key_name = aws_key_pair.myadmin_deploy_key.key_name\\n\\n root_block_device {\\n volume_size = 20 // Size in GB for root partition\\n volume_type = \\"gp2\\"\\n }\\n\\n user_data = <<-EOF\\n #!/bin/bash\\n sudo apt-get update\\n sudo apt-get install ca-certificates curl\\n sudo install -m 0755 -d /etc/apt/keyrings\\n sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc\\n sudo chmod a+r /etc/apt/keyrings/docker.asc\\n\\n # Add the repository to Apt sources:\\n echo \\\\\\n \\"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \\\\\\n $(. /etc/os-release && echo \\"$VERSION_CODENAME\\") stable\\" | \\\\\\n sudo tee /etc/apt/sources.list.d/docker.list > /dev/null\\n sudo apt-get update\\n\\n sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin\\n\\n systemctl start docker\\n systemctl enable docker\\n usermod -a -G docker ubuntu\\n EOF\\n\\n tags = {\\n Name = \\"myadmin-instance\\"\\n }\\n}\\n\\nresource \\"null_resource\\" \\"sync_files_and_run\\" {\\n # Use rsync to exclude node_modules, .git, db\\n provisioner \\"local-exec\\" {\\n # heredoc syntax\\n command = <<-EOF\\n rsync -t -av \\\\\\n --delete \\\\\\n --exclude \'node_modules\' \\\\\\n --exclude \'.git\' \\\\\\n --exclude \'.terraform\' \\\\\\n --exclude \'terraform*\' \\\\\\n --exclude \'.vscode\' \\\\\\n --exclude \'db\' \\\\\\n ./ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/\\n EOF\\n \\n }\\n\\n # Run docker compose after files have been copied\\n provisioner \\"remote-exec\\" {\\n inline = [\\n \\"bash -c \'while ! command -v docker &> /dev/null; do echo \\\\\\"Waiting for Docker to be installed...\\\\\\"; sleep 1; done\'\\",\\n \\"bash -c \'while ! docker info &> /dev/null; do echo \\\\\\"Waiting for Docker to start...\\\\\\"; sleep 1; done\'\\",\\n # -a would destroy cache\\n \\"docker system prune -f\\",\\n \\"cd /home/ubuntu/app/\\",\\n \\"docker compose -f compose.yml up --build -d\\"\\n ]\\n\\n connection {\\n type = \\"ssh\\"\\n user = \\"ubuntu\\"\\n private_key = file(\\"~/.ssh/id_rsa\\")\\n host = aws_eip_association.eip_assoc.public_ip\\n }\\n }\\n\\n # Ensure the resource is triggered every time based on timestamp or file hash\\n triggers = {\\n always_run = timestamp()\\n }\\n\\n depends_on = [aws_instance.myadmin_instance, aws_eip_association.eip_assoc]\\n}\\n\\n\\noutput \\"instance_public_ip\\" {\\n value = aws_eip_association.eip_assoc.public_ip\\n}\\n```\\n\\n\\nTo run the deployment first time, you need to run:\\n\\n```bash\\nterraform init\\n```\\n\\nThen with any change in code:\\n\\n```bash\\nterraform apply -auto-approve\\n```"},{"id":"ai-blog","metadata":{"permalink":"/blog/ai-blog","source":"@site/blog/2024-10-01-ai-blog/index.md","title":"Build AI-Assisted blog with AdminForth and Nuxt in 20 minutes","description":"Many developers today are using copilots to write code faster and relax their minds from a routine tasks.","date":"2024-10-01T00:00:00.000Z","tags":[{"inline":false,"label":"Nuxt.js","permalink":"/blog/tags/nuxt","description":"Nuxt.js is a free and open-source web application framework based on Vue.js, Node.js, Webpack, and Babel.js."},{"inline":false,"label":"ChatGPT","permalink":"/blog/tags/chatgpt","description":"ChatGPT is a conversational AI model that can generate human-like responses to text inputs."}],"readingTime":18.225,"hasTruncateMarker":false,"authors":[{"name":"Ivan Borshcho","title":"Maintainer of AdminForth","url":"https://github.com/ivictbor","imageURL":"https://avatars.githubusercontent.com/u/1838656?v=4","key":"ivanb"}],"frontMatter":{"slug":"ai-blog","title":"Build AI-Assisted blog with AdminForth and Nuxt in 20 minutes","authors":"ivanb","tags":["nuxt","chatgpt"]},"unlisted":false,"prevItem":{"title":"Deploy AdminForth to EC2 with terraform (without CI)","permalink":"/blog/compose-ec2-deployment"},"nextItem":{"title":"Chat-GPT plugin to co-write texts and strings","permalink":"/blog/chatgpt-plugin"}},"content":"Many developers today are using copilots to write code faster and relax their minds from a routine tasks.\\n\\nBut what about writing plain text? For example blogs and micro-blogs: sometimes you want to share your progress but you are lazy for typing. Then you can give a try to AI-assisted blogging. Our Open-Source AdminForth framework has couple of new AI-capable plugins to write text and generate images.\\n\\n![alt text](nuxtBlog.gif)\\n\\n\\nFor AI plugins are backed by OpenAI API, but their architecture allows to be easily extended for other AI providers once OpenAI competitors will reach the same or better level of quality.\\n\\nHere we will suggest you simple as 1-2-3 steps to build and host a blog with AI assistant which will help you to write posts.\\n\\nOur tech stack will include:\\n\\n- [Nuxt.js](https://nuxt.com/) - SEO-friendly page rendering framework\\n- [AdminForth](https://adminforth.dev/) - Admin panel framework for creating posts\\n- [AdminForth RichEditor plugin](https://adminforth.dev/docs/tutorial/Plugins/RichEditor/) - WYSIWYG editor with AI assistant in Copilot style\\n- Node and typescript\\n- Prisma for migrations\\n- SQLite for database, though you can easily switch it to Postgres or MongoDB\\n\\n## Prerequirements\\n\\nWe will use Node v20, if you not have it installed, we recommend [NVM](https://github.com/nvm-sh/nvm?tab=readme-ov-file#install--update-script)\\n\\n```bash\\nnvm install 20\\nnvm alias default 20\\nnvm use 20\\n```\\n\\n## Step 1: Create a new AdminForth project\\n\\n```bash\\nmkdir ai-blog\\ncd ai-blog\\nnpm init -y\\nnpm install adminforth @adminforth/upload @adminforth/rich-editor @adminforth/chat-gpt \\\\\\n express slugify http-proxy @types/express typescript tsx @types/node --save-dev\\nnpx --yes tsc --init --module NodeNext --target ESNext\\n```\\n\\n## Step 2: Prepare environment\\n\\n### OpenAI\\n\\nTo allocate OpenAI API key, go to https://platform.openai.com/, open Dashboard -> API keys -> Create new secret key.\\n\\n### S3\\n\\n1. Go to https://aws.amazon.com and login.\\n2. Go to Services -> S3 and create a bucket. Put in bucket name e.g. `my-ai-blog-bucket`. \\nFirst of all go to your bucket settings, Permissions, scroll down to Block public access (bucket settings for this bucket) and uncheck all checkboxes.\\nGo to bucket settings, Permissions, Object ownership and select \\"ACLs Enabled\\" and \\"Bucket owner preferred\\" radio buttons.\\n3. Go to bucket settings, Permissions, scroll down to Cross-origin resource sharing (CORS) and put in the following configuration:\\n\\n```json\\n[\\n {\\n \\"AllowedHeaders\\": [\\n \\"*\\"\\n ],\\n \\"AllowedMethods\\": [\\n \\"PUT\\"\\n ],\\n \\"AllowedOrigins\\": [\\n \\"http://localhost:3500\\"\\n ],\\n \\"ExposeHeaders\\": []\\n }\\n]\\n```\\n\\n> \u261d\ufe0f In AllowedOrigins add all your domains. For example if you will serve blog and admin on `https://blog.example.com/` you should add \\n> `\\"https://blog.example.com\\"` to AllowedOrigins:\\n>\\n> ```json\\n> [\\n> \\"https://blog.example.com\\",\\n> \\"http://localhost:3500\\"\\n> ]\\n> ```\\n> Every character matters, so don\'t forget to add `http://` or `https://` and don\'t add slashes at the end of the domain.\\n\\n4. Go to Services -> IAM and create a new user. Put in user name e.g. `my-ai-blog-bucket`.\\n5. Attach existing policies directly -> `AmazonS3FullAccess`. Go to your user -> `Add permissions` -> `Attach policies directly` -> `AmazonS3FullAccess`\\n6. Go to Security credentials and create a new access key. Save `Access key ID` and `Secret access key`.\\n\\n\\n### Create .env file in project directory\\n\\nCreate `.env` file with the following content:\\n\\n```bash title=\\".env\\"\\nDATABASE_URL=file:./db/db.sqlite\\nADMINFORTH_SECRET=\\nOPENAI_API_KEY=...\\nAWS_ACCESS_KEY_ID=your_access_key_id\\nAWS_SECRET_ACCESS_KEY=your_secret_access_key\\nAWS_S3_BUCKET=my-ai-blog-bucket\\nAWS_S3_REGION=us-east-1\\n```\\n\\n\\n## Step 3: Initialize database\\n\\nCreate `./schema.prisma` and put next content there:\\n\\n\\n```yaml title=\\"./schema.prisma\\" \\ngenerator client {\\n provider = \\"prisma-client-js\\"\\n}\\n\\ndatasource db {\\n provider = \\"sqlite\\"\\n url = env(\\"DATABASE_URL\\")\\n}\\n\\nmodel User {\\n id String @id\\n createdAt DateTime \\n email String @unique\\n avatar String?\\n publicName String?\\n passwordHash String\\n posts Post[]\\n}\\n\\nmodel Post {\\n id String @id\\n createdAt DateTime \\n title String\\n slug String\\n picture String?\\n content String\\n published Boolean \\n author User? @relation(fields: [authorId], references: [id])\\n authorId String?\\n contentImages ContentImage[]\\n}\\n\\nmodel ContentImage {\\n id String @id\\n createdAt DateTime \\n img String\\n postId String\\n resourceId String\\n post Post @relation(fields: [postId], references: [id])\\n}\\n```\\n\\nCreate database using `prisma migrate`:\\n\\n```bash\\nnpx -y prisma migrate dev --name init\\n```\\n\\n> in future if you will need to update schema, you can run `npx prisma migrate dev --name ` where `` is a name of migration.\\n\\n## Step 4: Setting up AdminForth\\n\\n\\nOpen `package.json`, set `type` to `module` and add `start` script:\\n\\n```json title=\\"./package.json\\"\\n{\\n ...\\n//diff-add\\n \\"type\\": \\"module\\",\\n \\"scripts\\": {\\n ...\\n//diff-add\\n \\"start\\": \\"NODE_ENV=development tsx watch --env-file=.env index.ts\\",\\n//diff-add\\n \\"startLive\\": \\"NODE_ENV=production APP_PORT=80 tsx index.ts\\"\\n },\\n}\\n```\\n\\nCreate `index.ts` file in root directory with following content:\\n\\n```ts title=\\"./index.ts\\"\\nimport express from \'express\';\\nimport AdminForth, { Filters, Sorts } from \'adminforth\';\\nimport userResource from \'./res/user.js\';\\nimport postResource from \'./res/posts.js\';\\nimport contentImageResource from \'./res/content-image.js\';\\nimport httpProxy from \'http-proxy\';\\n\\ndeclare var process : {\\n env: {\\n DATABASE_URL: string\\n NODE_ENV: string,\\n AWS_S3_BUCKET: string,\\n AWS_S3_REGION: string,\\n }\\n argv: string[]\\n}\\n\\nexport const admin = new AdminForth({\\n baseUrl: \'/admin\',\\n auth: {\\n usersResourceId: \'user\', // resource to get user during login\\n usernameField: \'email\', // field where username is stored, should exist in resource\\n passwordHashField: \'passwordHash\',\\n },\\n customization: {\\n brandName: \'My Admin\',\\n datesFormat: \'D MMM\',\\n timeFormat: \'HH:mm\',\\n emptyFieldPlaceholder: \'-\',\\n styles: {\\n colors: {\\n light: {\\n // color for links, icons etc.\\n primary: \'rgb(47 37 227)\',\\n // color for sidebar and text\\n sidebar: {main:\'#EFF5F7\', text:\'#333\'},\\n },\\n }\\n }\\n },\\n dataSources: [{\\n id: \'maindb\',\\n url: process.env.DATABASE_URL?.replace(\'file:\', \'sqlite://\'),\\n }],\\n resources: [\\n userResource,\\n postResource,\\n contentImageResource,\\n ],\\n menu: [\\n {\\n homepage: true,\\n label: \'Posts\',\\n icon: \'flowbite:home-solid\',\\n resourceId: \'post\',\\n },\\n { type: \'gap\' },\\n { type: \'divider\' },\\n { type: \'heading\', label: \'SYSTEM\' },\\n {\\n label: \'Users\',\\n icon: \'flowbite:user-solid\',\\n resourceId: \'user\',\\n }\\n ],\\n});\\n\\n\\nif (import.meta.url === `file://${process.argv[1]}`) {\\n // if script is executed directly e.g. node index.ts or npm start\\n\\n const app = express()\\n app.use(express.json());\\n const port = 3500;\\n\\n // needed to compile SPA. Call it here or from a build script e.g. in Docker build time to reduce downtime\\n if (process.env.NODE_ENV === \'development\') {\\n await admin.bundleNow({ hotReload: true });\\n }\\n console.log(\'Bundling AdminForth done. For faster serving consider calling bundleNow() from a build script.\');\\n\\n // api to server recent posts\\n app.get(\'/api/posts\', async (req, res) => {\\n const { offset = 0, limit = 100, slug = null } = req.query;\\n const posts = await admin.resource(\'post\').list(\\n [Filters.EQ(\'published\', true), ...(slug ? [Filters.LIKE(\'slug\', slug)] : [])],\\n limit,\\n offset,\\n Sorts.DESC(\'createdAt\'),\\n );\\n const authorIds = [...new Set(posts.map((p: any) => p.authorId))];\\n const authors = (await admin.resource(\'user\').list(Filters.IN(\'id\', authorIds)))\\n .reduce((acc: any, a: any) => {acc[a.id] = a; return acc;}, {});\\n posts.forEach((p: any) => {\\n const author = authors[p.authorId];\\n p.author = { \\n publicName: author.publicName, \\n avatar: `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_S3_REGION}.amazonaws.com/${author.avatar}`\\n };\\n p.picture = `https://${process.env.AWS_S3_BUCKET}.s3.${process.env.AWS_S3_REGION}.amazonaws.com/${p.picture}`;\\n });\\n res.json(posts);\\n });\\n\\n // here we proxy all non-/admin requests to nuxt instance http://localhost:3000\\n // this is done for demo purposes, in production you should do this using high-performance reverse proxy like traefik or nginx\\n app.use((req, res, next) => {\\n if (!req.url.startsWith(\'/admin\')) {\\n const proxy = httpProxy.createProxyServer();\\n proxy.on(\'error\', function (err, req, res) {\\n res.send(`No response from Nuxt at http://localhost:3000, did you start it? ${err}`)\\n });\\n proxy.web(req, res, { target: \'http://localhost:3000\' });\\n } else {\\n next();\\n }\\n });\\n\\n // serve after you added all api\\n admin.express.serve(app)\\n\\n admin.discoverDatabases().then(async () => {\\n if (!await admin.resource(\'user\').get([Filters.EQ(\'email\', \'adminforth@adminforth.dev\')])) {\\n await admin.resource(\'user\').create({\\n email: \'adminforth@adminforth.dev\',\\n passwordHash: await AdminForth.Utils.generatePasswordHash(\'adminforth\'),\\n });\\n }\\n });\\n\\n app.listen(port, () => {\\n console.log(`\\\\n\u26a1 AdminForth is available at http://localhost:${port}\\\\n`)\\n });\\n}\\n```\\n\\n## Step 5: Create resources\\n\\nCreate `res` folder. Create `./res/user.ts` file with following content:\\n\\n```ts title=\\"./res/users.ts\\"\\nimport AdminForth, { AdminForthDataTypes } from \'adminforth\';\\nimport { randomUUID } from \'crypto\';\\nimport UploadPlugin from \'@adminforth/upload\';\\n\\nexport default {\\n dataSource: \'maindb\',\\n table: \'user\',\\n label: \'Users\',\\n recordLabel: (r: any) => `\ud83d\udc64 ${r.email}`,\\n columns: [\\n {\\n name: \'id\',\\n primaryKey: true,\\n fillOnCreate: () => randomUUID(),\\n showIn: [\'list\', \'filter\', \'show\'],\\n },\\n {\\n name: \'email\',\\n required: true,\\n isUnique: true,\\n enforceLowerCase: true,\\n validation: [\\n AdminForth.Utils.EMAIL_VALIDATOR,\\n ],\\n type: AdminForthDataTypes.STRING,\\n },\\n {\\n name: \'createdAt\',\\n type: AdminForthDataTypes.DATETIME,\\n showIn: [\'list\', \'filter\', \'show\'],\\n fillOnCreate: () => (new Date()).toISOString(),\\n },\\n {\\n name: \'password\',\\n virtual: true,\\n required: { create: true },\\n editingNote: { edit: \'Leave empty to keep password unchanged\' },\\n minLength: 8,\\n type: AdminForthDataTypes.STRING,\\n showIn: [\'create\', \'edit\'],\\n masked: true,\\n validation: [\\n // request to have at least 1 digit, 1 upper case, 1 lower case\\n AdminForth.Utils.PASSWORD_VALIDATORS.UP_LOW_NUM,\\n ],\\n },\\n { name: \'passwordHash\', backendOnly: true, showIn: [] },\\n { \\n name: \'publicName\',\\n type: AdminForthDataTypes.STRING,\\n },\\n { name: \'avatar\' },\\n ],\\n hooks: {\\n create: {\\n beforeSave: async ({ record, adminUser, resource }) => {\\n record.passwordHash = await AdminForth.Utils.generatePasswordHash(record.password);\\n return { ok: true };\\n }\\n },\\n edit: {\\n beforeSave: async ({ record, adminUser, resource }) => {\\n if (record.password) {\\n record.passwordHash = await AdminForth.Utils.generatePasswordHash(record.password);\\n }\\n return { ok: true }\\n },\\n },\\n }\\n plugins: [\\n new UploadPlugin({\\n pathColumnName: \'avatar\',\\n s3Bucket: process.env.AWS_S3_BUCKET,\\n s3Region: process.env.AWS_S3_REGION,\\n allowedFileExtensions: [\'jpg\', \'jpeg\', \'png\', \'gif\', \'webm\',\'webp\'],\\n maxFileSize: 1024 * 1024 * 20, // 20MB\\n s3AccessKeyId: process.env.AWS_ACCESS_KEY_ID,\\n s3SecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\\n s3ACL: \'public-read\', // ACL which will be set to uploaded file\\n s3Path: (\\n { originalFilename, originalExtension }: {originalFilename: string, originalExtension: string }\\n ) => `user-avatars/${new Date().getFullYear()}/${randomUUID()}/${originalFilename}.${originalExtension}`,\\n generation: {\\n provider: \'openai-dall-e\',\\n countToGenerate: 2,\\n openAiOptions: {\\n model: \'dall-e-3\',\\n size: \'1024x1024\',\\n apiKey: process.env.OPENAI_API_KEY,\\n },\\n },\\n }),\\n ],\\n}\\n```\\n\\n\\nCreate `posts.ts` file in res directory with following content:\\n\\n```ts title=\\"./res/post.ts\\"\\nimport { AdminUser, AdminForthDataTypes } from \'adminforth\';\\nimport { randomUUID } from \'crypto\';\\nimport UploadPlugin from \'@adminforth/upload\';\\nimport RichEditorPlugin from \'@adminforth/rich-editor\';\\nimport ChatGptPlugin from \'@adminforth/chat-gpt\';\\nimport slugify from \'slugify\';\\n\\nexport default {\\n table: \'post\',\\n dataSource: \'maindb\',\\n label: \'Posts\',\\n recordLabel: (r: any) => `\ud83d\udcdd ${r.title}`,\\n columns: [\\n {\\n name: \'id\',\\n primaryKey: true,\\n fillOnCreate: () => randomUUID(),\\n showIn: [\'filter\', \'show\'],\\n },\\n {\\n name: \'title\',\\n required: true,\\n showIn: [\'list\', \'create\', \'edit\', \'filter\', \'show\'],\\n maxLength: 255,\\n minLength: 3,\\n type: AdminForthDataTypes.STRING,\\n },\\n {\\n name: \'picture\',\\n showIn: [\'list\', \'create\', \'edit\', \'filter\', \'show\'],\\n },\\n {\\n name: \'slug\',\\n showIn: [\'filter\', \'show\'],\\n },\\n {\\n name: \'content\',\\n showIn: [\'create\', \'edit\', \'filter\', \'show\'],\\n type: AdminForthDataTypes.RICHTEXT,\\n },\\n {\\n name: \'createdAt\',\\n showIn: [\'list\', \'filter\', \'show\',],\\n fillOnCreate: () => (new Date()).toISOString(),\\n },\\n {\\n name: \'published\',\\n required: true,\\n },\\n {\\n name: \'authorId\',\\n foreignResource: {\\n resourceId: \'user\',\\n },\\n showIn: [\'filter\', \'show\'],\\n fillOnCreate: ({ adminUser }: { adminUser: AdminUser }) => {\\n return adminUser.dbUser.id;\\n }\\n }\\n ],\\n hooks: {\\n create: {\\n beforeSave: async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {\\n record.slug = slugify(record.title, { lower: true });\\n return { ok: true };\\n },\\n },\\n edit: {\\n beforeSave: async ({ record, adminUser }: { record: any, adminUser: AdminUser }) => {\\n if (record.title) {\\n record.slug = slugify(record.title, { lower: true });\\n }\\n return { ok: true };\\n },\\n },\\n },\\n plugins: [\\n new UploadPlugin({\\n pathColumnName: \'picture\',\\n s3Bucket: process.env.AWS_S3_BUCKET,\\n s3Region: process.env.AWS_S3_REGION,\\n allowedFileExtensions: [\'jpg\', \'jpeg\', \'png\', \'gif\', \'webm\',\'webp\'],\\n maxFileSize: 1024 * 1024 * 20, // 20MB\\n s3AccessKeyId: process.env.AWS_ACCESS_KEY_ID,\\n s3SecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\\n s3ACL: \'public-read\', // ACL which will be set to uploaded file\\n s3Path: (\\n { originalFilename, originalExtension }: {originalFilename: string, originalExtension: string }\\n ) => `post-previews/${new Date().getFullYear()}/${randomUUID()}/${originalFilename}.${originalExtension}`,\\n generation: {\\n provider: \'openai-dall-e\',\\n countToGenerate: 2,\\n openAiOptions: {\\n model: \'dall-e-3\',\\n size: \'1792x1024\',\\n apiKey: process.env.OPENAI_API_KEY,\\n },\\n fieldsForContext: [\'title\'],\\n },\\n }),\\n new RichEditorPlugin({\\n htmlFieldName: \'content\',\\n completion: {\\n provider: \'openai-chat-gpt\',\\n params: {\\n apiKey: process.env.OPENAI_API_KEY,\\n model: \'gpt-4o\',\\n },\\n expert: {\\n debounceTime: 250,\\n }\\n }, \\n attachments: {\\n attachmentResource: \'contentImage\',\\n attachmentFieldName: \'img\',\\n attachmentRecordIdFieldName: \'postId\',\\n attachmentResourceIdFieldName: \'resourceId\',\\n },\\n }),\\n new ChatGptPlugin({\\n openAiApiKey: process.env.OPENAI_API_KEY,\\n model: \'gpt-4o\',\\n fieldName: \'title\',\\n expert: {\\n debounceTime: 250,\\n }\\n }),\\n ]\\n}\\n```\\n\\nAlso create `content-image.ts` file in `res` directory with following content:\\n\\n```ts title=\\"./res/content-image.ts\\"\\n\\nimport { AdminForthDataTypes } from \'adminforth\';\\nimport { randomUUID } from \'crypto\';\\nimport UploadPlugin from \'@adminforth/upload\';\\n\\nexport default {\\n table: \'contentImage\',\\n dataSource: \'maindb\',\\n label: \'Content Images\',\\n recordLabel: (r: any) => `\ud83d\uddbc\ufe0f ${r.img}`,\\n columns: [\\n {\\n name: \'id\',\\n primaryKey: true,\\n fillOnCreate: () => randomUUID(),\\n },\\n {\\n name: \'createdAt\',\\n type: AdminForthDataTypes.DATETIME,\\n fillOnCreate: () => (new Date()).toISOString(),\\n },\\n {\\n name: \'img\',\\n type: AdminForthDataTypes.STRING,\\n required: true,\\n },\\n {\\n name: \'postId\',\\n foreignResource: {\\n resourceId: \'post\',\\n },\\n showIn: [\'list\', \'filter\', \'show\'],\\n },\\n {\\n name: \'resourceId\',\\n }\\n ],\\n plugins: [\\n new UploadPlugin({\\n pathColumnName: \'img\',\\n s3Bucket: process.env.AWS_S3_BUCKET,\\n s3Region: process.env.AWS_S3_REGION,\\n allowedFileExtensions: [\'jpg\', \'jpeg\', \'png\', \'gif\', \'webm\',\'webp\'],\\n maxFileSize: 1024 * 1024 * 20, // 20MB\\n s3AccessKeyId: process.env.AWS_ACCESS_KEY_ID,\\n s3SecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,\\n s3ACL: \'public-read\', // ACL which will be set to uploaded file\\n s3Path: (\\n { originalFilename, originalExtension }: {originalFilename: string, originalExtension: string }\\n ) => `post-content/${new Date().getFullYear()}/${randomUUID()}/${originalFilename}.${originalExtension}`,\\n }),\\n ],\\n}\\n```\\n\\nNow you can start your admin panel:\\n\\n```bash\\nnpm start\\n```\\n\\nOpen `http://localhost:3500/admin` in your browser and login with `adminforth@adminforth.dev` and `adminforth` credentials.\\nSet up your avatar (you can generate it with AI) and public name in user settings.\\n\\n![alt text](aiblogpost.png)\\n\\n## Step 5: Create Nuxt project\\n\\n\\nNow let\'s initialize our seo-facing frontend:\\n\\n```bash\\nnpx nuxi@latest init seo\\ncd seo\\nnpm install -D sass-embedded\\nnpm run dev\\n```\\n\\nEdit `app.vue`: \\n\\n```html title=\\"./seo/app.vue\\"\\n\\n\\n\\n\\n```\\n\\n\\nAdd folder `pages` and create `index.vue`:\\n\\n```html title=\\"./seo/pages/index.vue\\"\\n\\n\\n\\n\\n - - + +

Build AI-Assisted blog with AdminForth and Nuxt in 20 minutes

· 19 min read
Ivan Borshcho
Maintainer of AdminForth

Many developers today are using copilots to write code faster and relax their minds from a routine tasks.

diff --git a/blog/archive/index.html b/blog/archive/index.html index 38315130c..16aacea3c 100644 --- a/blog/archive/index.html +++ b/blog/archive/index.html @@ -15,8 +15,8 @@ - - + + diff --git a/blog/atom.xml b/blog/atom.xml index 21e968691..bb3c1bceb 100644 --- a/blog/atom.xml +++ b/blog/atom.xml @@ -31,8 +31,8 @@

Create deploy/.gitignore file with next content:

.terraform/
.keys/
*.tfstate
*.tfstate.*
*.tfvars
tfplan

Step 5 - Main terraform file main.tf

-

Create file main.tf:

-
main.tf

locals {
app_name = "<your_app_name>"
aws_region = "eu-central-1"
}


provider "aws" {
region = local.aws_region
profile = "myaws"
}

data "aws_ami" "ubuntu_linux" {
most_recent = true
owners = ["amazon"]

filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
}

data "aws_vpc" "default" {
default = true
}


resource "aws_eip" "eip" {
vpc = true
}
resource "aws_eip_association" "eip_assoc" {
instance_id = aws_instance.app_instance.id
allocation_id = aws_eip.eip.id
}

data "aws_subnet" "default_subnet" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}

filter {
name = "default-for-az"
values = ["true"]
}

filter {
name = "availability-zone"
values = ["${local.aws_region}a"]
}
}

resource "aws_security_group" "instance_sg" {
name = "${local.app_name}-instance-sg"
vpc_id = data.aws_vpc.default.id

ingress {
description = "Allow HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

# SSH
ingress {
description = "Allow SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_key_pair" "app_deployer" {
key_name = "terraform-deploy_${local.app_name}-key"
public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key
}

resource "aws_instance" "app_instance" {
ami = data.aws_ami.ubuntu_linux.id
instance_type = "t3a.small"
subnet_id = data.aws_subnet.default_subnet.id
vpc_security_group_ids = [aws_security_group.instance_sg.id]
key_name = aws_key_pair.app_deployer.key_name

root_block_device {
volume_size = 40 // Size in GB for root partition
volume_type = "gp2"
}

user_data = <<-EOF
#!/bin/bash
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

systemctl start docker
systemctl enable docker
usermod -a -G docker ubuntu
EOF

tags = {
Name = "${local.app_name}-instance"
}
}

resource "null_resource" "sync_files_and_run" {
# Use rsync to exclude node_modules, .git, db
provisioner "local-exec" {
# heredoc syntax
# remove files that where deleted on the source
command = <<-EOF
# -o StrictHostKeyChecking=no
rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \
--delete \
--exclude 'node_modules' \
--exclude '.git' \
--exclude '.terraform' \
--exclude 'terraform*' \
--exclude 'tfplan' \
--exclude '.keys' \
--exclude '.vscode' \
--exclude '.env' \
--exclude 'db' \
--exclude 'up-human/debug' \
--exclude 'up-human/storage' \
../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/
EOF
}

# Run docker compose after files have been copied
provisioner "remote-exec" {
inline = [
# fail bash specially and intentionally to stop the script on error
"bash -c 'while ! command -v docker &> /dev/null; do echo \"Waiting for Docker to be installed...\"; sleep 1; done'",
"bash -c 'while ! docker info &> /dev/null; do echo \"Waiting for Docker to start...\"; sleep 1; done'",

# please note that prune might destroy build cache and make build slower, however it releases disk space
"docker system prune -f",
# "docker buildx prune -f --filter 'type!=exec.cachemount'",
"cd /home/ubuntu/app/deploy",
"docker compose -p app -f compose.yml up --build -d"
]

connection {
type = "ssh"
user = "ubuntu"
private_key = file("./.keys/id_rsa")
host = aws_eip_association.eip_assoc.public_ip
}
}

# Ensure the resource is triggered every time based on timestamp or file hash
triggers = {
always_run = timestamp()
}

depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]
}


output "instance_public_ip" {
value = aws_eip_association.eip_assoc.public_ip
}


######### This scetion is for tf state storage ##############

# S3 bucket for storing Terraform state
resource "aws_s3_bucket" "terraform_state" {
bucket = "${local.app_name}-terraform-state"
}

resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.bucket

rule {
status = "Enabled"
id = "Keep only the latest version of the state file"

noncurrent_version_expiration {
noncurrent_days = 30
}
}
}

resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.bucket

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.bucket

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_lock" {
name = "${local.app_name}-terraform-lock-table"
billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand

hash_key = "LockID" # Primary key for the table

attribute {
name = "LockID"
type = "S"
}
}

+

Create file main.tf in deploy folder:

+
deploy/main.tf

locals {
app_name = "<your_app_name>"
aws_region = "eu-central-1"
}


provider "aws" {
region = local.aws_region
profile = "myaws"
}

data "aws_ami" "ubuntu_linux" {
most_recent = true
owners = ["amazon"]

filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
}

data "aws_vpc" "default" {
default = true
}


resource "aws_eip" "eip" {
vpc = true
}
resource "aws_eip_association" "eip_assoc" {
instance_id = aws_instance.app_instance.id
allocation_id = aws_eip.eip.id
}

data "aws_subnet" "default_subnet" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}

filter {
name = "default-for-az"
values = ["true"]
}

filter {
name = "availability-zone"
values = ["${local.aws_region}a"]
}
}

resource "aws_security_group" "instance_sg" {
name = "${local.app_name}-instance-sg"
vpc_id = data.aws_vpc.default.id

ingress {
description = "Allow HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

# SSH
ingress {
description = "Allow SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

resource "aws_key_pair" "app_deployer" {
key_name = "terraform-deploy_${local.app_name}-key"
public_key = file("./.keys/id_rsa.pub") # Path to your public SSH key
}

resource "aws_instance" "app_instance" {
ami = data.aws_ami.ubuntu_linux.id
instance_type = "t3a.small"
subnet_id = data.aws_subnet.default_subnet.id
vpc_security_group_ids = [aws_security_group.instance_sg.id]
key_name = aws_key_pair.app_deployer.key_name

root_block_device {
volume_size = 40 // Size in GB for root partition
volume_type = "gp2"
}

user_data = <<-EOF
#!/bin/bash
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

systemctl start docker
systemctl enable docker
usermod -a -G docker ubuntu
EOF

tags = {
Name = "${local.app_name}-instance"
}
}

resource "null_resource" "sync_files_and_run" {
# Use rsync to exclude node_modules, .git, db
provisioner "local-exec" {
# heredoc syntax
# remove files that where deleted on the source
command = <<-EOF
# -o StrictHostKeyChecking=no
rsync -t -av -e "ssh -i ./.keys/id_rsa -o StrictHostKeyChecking=no" \
--delete \
--exclude 'node_modules' \
--exclude '.git' \
--exclude '.terraform' \
--exclude 'terraform*' \
--exclude 'tfplan' \
--exclude '.keys' \
--exclude '.vscode' \
--exclude '.env' \
--exclude 'db' \
--exclude 'up-human/debug' \
--exclude 'up-human/storage' \
../ ubuntu@${aws_eip_association.eip_assoc.public_ip}:/home/ubuntu/app/
EOF
}

# Run docker compose after files have been copied
provisioner "remote-exec" {
inline = [
# fail bash specially and intentionally to stop the script on error
"bash -c 'while ! command -v docker &> /dev/null; do echo \"Waiting for Docker to be installed...\"; sleep 1; done'",
"bash -c 'while ! docker info &> /dev/null; do echo \"Waiting for Docker to start...\"; sleep 1; done'",

# please note that prune might destroy build cache and make build slower, however it releases disk space
"docker system prune -f",
# "docker buildx prune -f --filter 'type!=exec.cachemount'",
"cd /home/ubuntu/app/deploy",
"docker compose -p app -f compose.yml up --build -d"
]

connection {
type = "ssh"
user = "ubuntu"
private_key = file("./.keys/id_rsa")
host = aws_eip_association.eip_assoc.public_ip
}
}

# Ensure the resource is triggered every time based on timestamp or file hash
triggers = {
always_run = timestamp()
}

depends_on = [aws_instance.app_instance, aws_eip_association.eip_assoc]
}


output "instance_public_ip" {
value = aws_eip_association.eip_assoc.public_ip
}


######### This scetion is for tf state storage ##############

# S3 bucket for storing Terraform state
resource "aws_s3_bucket" "terraform_state" {
bucket = "${local.app_name}-terraform-state"
}

resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.bucket

rule {
status = "Enabled"
id = "Keep only the latest version of the state file"

noncurrent_version_expiration {
noncurrent_days = 30
}
}
}

resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.bucket

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.bucket

rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_lock" {
name = "${local.app_name}-terraform-lock-table"
billing_mode = "PAY_PER_REQUEST" # Dynamically scales to meet demand

hash_key = "LockID" # Primary key for the table

attribute {
name = "LockID"
type = "S"
}
}

👆 Replace <your_app_name> with your app name (no spaces, only underscores or letters)

@@ -59,10 +59,10 @@ Unfortunately we can't use variables, HashiCorp thinks it is too dangerous 😥<

Step 7 - CI/CD - Github Actions

Create file .github/workflows/deploy.yml:

.github/workflows/deploy.yml
name: Deploy 
run-name: ${{ github.actor }} builds app 🚀
on: [push]
jobs:
Explore-GitHub-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server"
- run: echo "🔎 The name of your branch is ${{ github.ref }}"
- name: Check out repository code
uses: actions/checkout@v4
- name: Set up Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.4.6
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- name: Start building
env:
VAULT_AWS_ACCESS_KEY_ID: ${{ secrets.VAULT_AWS_ACCESS_KEY_ID }}
VAULT_AWS_SECRET_ACCESS_KEY: ${{ secrets.VAULT_AWS_SECRET_ACCESS_KEY }}
VAULT_SSH_PRIVATE_KEY: ${{ secrets.VAULT_SSH_PRIVATE_KEY }}
VAULT_SSH_PUBLIC_KEY: ${{ secrets.VAULT_SSH_PUBLIC_KEY }}
run: |
/bin/sh -x deploy/deploy.sh

- run: echo "🍏 This job's status is ${{ job.status }}."
-

Step 6.1 - Create deploy script

+

Step 7.1 - Create deploy script

Now create file deploy/deploy.sh:

deploy/deploy.sh

# cd to dir of script
cd "$(dirname "$0")"

mkdir -p ~/.aws ./.keys

cat <<EOF > ~/.aws/credentials
[myaws]
aws_access_key_id=$VAULT_AWS_ACCESS_KEY_ID
aws_secret_access_key=$VAULT_AWS_SECRET_ACCESS_KEY
EOF

cat <<EOF > ./.keys/id_rsa
$VAULT_SSH_PRIVATE_KEY
EOF

cat <<EOF > ./.keys/id_rsa.pub
$VAULT_SSH_PUBLIC_KEY
EOF

chmod 600 ./.keys/id_rsa*

# force Terraform to reinitialize the backend without migrating the state.
terraform init -reconfigure
terraform plan -out=tfplan
terraform apply tfplan
-

Step 6.2 - Add secrets to GitHub

+

Step 7.2 - Add secrets to GitHub

Go to your GitHub repository, then Settings -> Secrets -> New repository secret and add: