@appxdigital/appx-core-cli 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@appxdigital/appx-core-cli",
4
- "version": "1.0.6",
4
+ "version": "1.0.7",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
7
  "npm:publish": "npm publish --access public",
@@ -0,0 +1,25 @@
1
+ import { AdminConfigType } from '@appxdigital/appx-core';
2
+ import { ComponentLoader } from 'adminjs';
3
+
4
+ const componentLoader = new ComponentLoader();
5
+
6
+ export const AdminConfig: AdminConfigType = {
7
+ componentLoader,
8
+ resources: [
9
+ {
10
+ name: 'User',
11
+ },
12
+ ],
13
+ rootPath: '/admin',
14
+ branding: {
15
+ companyName: 'AppX Core',
16
+ logo: 'https://appx-website-assets.fra1.cdn.digitaloceanspaces.com/2024/04/logo_color.svg',
17
+ },
18
+ // As you can see below, you can customize the dashboard component, which is the first page you see when you access the AdminJS
19
+ dashboard: {
20
+ component: componentLoader.add(
21
+ 'Dashboard',
22
+ '../backoffice/components/dashboard',
23
+ ),
24
+ },
25
+ };
@@ -1,37 +1,10 @@
1
1
  import React from 'react';
2
2
 
3
3
  export const Dashboard = () => {
4
+ // Redirect to Users resource
5
+ window.location.href = window.location.href + '/resources/User';
4
6
 
5
- return (
6
- <div style={{
7
- backgroundColor: 'white',
8
- borderRadius: '15px',
9
- height: '100%',
10
- padding: '1rem',
11
- margin: '1rem',
12
- }}>
13
- <h1 style={{ fontSize: "1.5rem", textAlign: "center" }}>Dashboard</h1>
14
- <div style={{
15
- display: 'flex',
16
- gap: '1rem',
17
- flexDirection: 'column',
18
- textAlign: 'center',
19
- marginTop: '3rem',
20
- borderRadius: '15px',
21
- border: '2px solid gainsboro',
22
- maxWidth: '300px',
23
- margin: '3rem auto',
24
- padding: '1rem',
25
- }}>
26
- <h2>Customize it!</h2>
27
- <p>You can customize your dashboard however you want.</p>
28
- <p>Display any data you might find useful.</p>
29
- <p>See the number of users or check statistics on graphics</p>
30
- <p>Whatever you want to do! Edit it on components/dashboard.</p>
31
- </div>
32
- </div>
33
-
34
- );
7
+ return null;
35
8
  };
36
9
 
37
10
  export default Dashboard;
@@ -1,27 +1,39 @@
1
- import {
2
- MiddlewareConsumer, Module, NestModule, RequestMethod,
3
- } from '@nestjs/common';
4
- import { AppController } from './app.controller';
5
- import { AppService } from './app.service';
6
- import { ConfigModule } from '@nestjs/config';
7
- import {AuthModule, PrismaInterceptor, AppxCoreModule} from '@appxdigital/appx-core';
8
- import { APP_INTERCEPTOR } from '@nestjs/core';
9
- import {
10
- RequestContextModule, RequestContextMiddleware,
11
- } from 'nestjs-request-context'
12
- import { PermissionsConfig } from './config/permissions.config';
1
+ import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/common';
2
+ import {AppController} from './app.controller';
3
+ import {AppService} from './app.service';
4
+ import {ConfigModule} from '@nestjs/config';
5
+ import {AuthModule, PrismaInterceptor, AppxCoreModule, AppxCoreAdminModule} from '@appxdigital/appx-core';
6
+ import {APP_INTERCEPTOR} from '@nestjs/core';
7
+ import {RequestContextModule, RequestContextMiddleware} from 'nestjs-request-context'
8
+ import {PermissionsConfig} from './config/permissions.config';
9
+ import {AdminConfig} from './config/admin.config';
13
10
 
14
11
  @Module({
15
- imports: [RequestContextModule, ConfigModule.forRoot({
16
- isGlobal: true, expandVariables: true, envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
17
- }), AppxCoreModule.forRoot(PermissionsConfig), RequestContextModule, AuthModule,], controllers: [AppController], providers: [AppService, {
18
- provide: APP_INTERCEPTOR, useClass: PrismaInterceptor,
19
- },],
12
+ imports: [
13
+ RequestContextModule,
14
+ ConfigModule.forRoot({
15
+ isGlobal: true,
16
+ expandVariables: true,
17
+ envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
18
+ }),
19
+ AppxCoreModule.forRoot(PermissionsConfig),
20
+ AppxCoreAdminModule.forRoot(AdminConfig, PermissionsConfig),
21
+ RequestContextModule,
22
+ AuthModule,
23
+ ],
24
+ controllers: [AppController],
25
+ providers: [
26
+ AppService,
27
+ {
28
+ provide: APP_INTERCEPTOR,
29
+ useClass: PrismaInterceptor,
30
+ },
31
+ ],
20
32
  })
21
33
  export class AppModule implements NestModule {
22
- configure(consumer: MiddlewareConsumer) {
34
+ configure (consumer: MiddlewareConsumer) {
23
35
  consumer
24
36
  .apply(RequestContextMiddleware)
25
- .forRoutes({ path: '*', method: RequestMethod.ALL });
37
+ .forRoutes({path: '*', method: RequestMethod.ALL});
26
38
  }
27
39
  }
@@ -1,17 +1,20 @@
1
- import {PermissionsConfigType} from '@appxdigital/appx-core';
1
+ import {
2
+ PermissionsConfigType,
3
+ PermissionPlaceholder,
4
+ } from '@appxdigital/appx-core';
2
5
 
3
6
  export const PermissionsConfig: PermissionsConfigType = {
4
7
  User: {
5
8
  ADMIN: {
6
- findUnique: 'ALL',
9
+ findFirst: 'ALL',
7
10
  findMany: 'ALL',
8
11
  create: 'ALL',
9
- update: 'ALL',
10
- delete: 'ALL',
12
+ updateMany: 'ALL',
13
+ deleteMany: 'ALL',
11
14
  },
12
15
  CLIENT: {
13
- findUnique: {
14
- conditions: { id: '$USER_ID' }
16
+ findFirst: {
17
+ conditions: { id: PermissionPlaceholder.USER_ID },
15
18
  },
16
19
  }
17
20
  },
package/wizard.js CHANGED
@@ -225,12 +225,19 @@ APP_PORT=3000
225
225
 
226
226
  #Default behaviour for use of transactions
227
227
  USE_TRANSACTION=true
228
+
228
229
  #Session secret
229
- SESSION_SECRET="DEFAULT_SECRET"
230
+ SESSION_SECRET="${require('crypto').randomBytes(32).toString('hex')}"
231
+
230
232
  #Cookie name for the session token
231
233
  SESSION_COOKIE_NAME="APPXCORE"
234
+
232
235
  #Expiration time for the session token in seconds
233
236
  SESSION_TTL=86400
237
+
238
+ # JWT
239
+ JWT_SECRET="${require('crypto').randomBytes(32).toString('hex')}"
240
+ JWT_REFRESH_SECRET="${require('crypto').randomBytes(32).toString('hex')}"
234
241
  `;
235
242
 
236
243
  fs.writeFileSync(`${projectPath}/.env`, envContent);
@@ -262,9 +269,6 @@ SESSION_TTL=86400
262
269
  }
263
270
 
264
271
  function setupProjectStructure(projectPath, answers) {
265
-
266
- const includeBackoffice = answers?.backoffice;
267
-
268
272
  ensureAndRunNestCli(projectPath, answers?.showOutput);
269
273
  incrementProgress(answers?.showOutput, "nestjs");
270
274
 
@@ -288,7 +292,7 @@ function setupProjectStructure(projectPath, answers) {
288
292
  console.warn('Could not find generated tsconfig.json to modify.');
289
293
  }
290
294
 
291
- const appModuleContent = includeBackoffice ? getAdminAppModuleTemplate() : getAppModuleTemplate();
295
+ const appModuleContent = getAppModuleTemplate();
292
296
  fs.writeFileSync(`${projectPath}/src/app.module.ts`, appModuleContent);
293
297
  incrementProgress(answers?.showOutput, "appmodule");
294
298
  installDependenciesFromManifest(answers?.showOutput);
@@ -314,7 +318,7 @@ function setupProjectStructure(projectPath, answers) {
314
318
  addScriptsToPackageJson(projectPath);
315
319
 
316
320
  if (includeBackoffice) {
317
- executeCommand('npm install adminjs @adminjs/express @adminjs/nestjs @adminjs/prisma @prisma/sdk react express-formidable @adminjs/passwords', answers?.showOutput);
321
+ executeCommand('npm install adminjs @adminjs/express @adminjs/import-export @adminjs/nestjs @adminjs/prisma @prisma/sdk react express-formidable @adminjs/passwords', answers?.showOutput);
318
322
  setupAdminJS(projectPath);
319
323
  incrementProgress(answers?.showOutput, "adminSetup");
320
324
  }
@@ -397,11 +401,6 @@ function getAdminTemplate() {
397
401
  return fs.readFileSync(template, 'utf8');
398
402
  }
399
403
 
400
- function getAdminAppModuleTemplate() {
401
- const appModuleTemplatePath = path.join(__dirname, 'templates', 'adminjs', 'adminjs-app.module.template.js');
402
- return fs.readFileSync(appModuleTemplatePath, 'utf8');
403
- }
404
-
405
404
  function createPermissionsConfig(projectPath) {
406
405
  const configDir = path.join(projectPath, 'src/config');
407
406
  fs.ensureDirSync(configDir);
@@ -412,6 +411,16 @@ function createPermissionsConfig(projectPath) {
412
411
  fs.writeFileSync(path.join(configDir, 'permissions.config.ts'), permissionsConfigContent);
413
412
  }
414
413
 
414
+ function createAdminConfig(projectPath) {
415
+ const configDir = path.join(projectPath, 'src/config');
416
+ fs.ensureDirSync(configDir);
417
+
418
+ const templatePath = path.join(__dirname, 'templates', 'admin.config.template.js');
419
+ const permissionsConfigContent = fs.readFileSync(templatePath, 'utf-8');
420
+
421
+ fs.writeFileSync(path.join(configDir, 'admin.config.ts'), permissionsConfigContent);
422
+ }
423
+
415
424
  function customizePrismaSchema(projectPath, answers) {
416
425
  const prismaDir = `${projectPath}/prisma`;
417
426
  const schemaFile = `${prismaDir}/schema.prisma`;
@@ -1,143 +0,0 @@
1
- import { DynamicModule } from '@nestjs/common';
2
- import { addBasicFilters, createActions, createPermissionHandler, dynamicImport, getAdminJSResources } from './utils';
3
- import { initializeComponents } from './component-loader';
4
- import { readFileSync } from 'fs';
5
- import { getDMMF } from '@prisma/sdk';
6
- import {PrismaService} from '@appxdigital/appx-core';
7
- import { PrismaModule } from "../prisma/prisma.module";
8
-
9
- const DEFAULT_ADMIN = {
10
- email: 'joao.duvido@appx.pt',
11
- password: 'password',
12
- };
13
-
14
- const authenticate = async (email: string, password: string) => {
15
- if (email === DEFAULT_ADMIN.email && password === DEFAULT_ADMIN.password) {
16
- return Promise.resolve(DEFAULT_ADMIN);
17
- }
18
- return null;
19
- };
20
-
21
- export async function createAdminJsModule(): Promise<DynamicModule> {
22
- // Due to AdminJS only allowing ESM now, we need to use dynamic imports to load the modules, this function can be found within src/backoffice/utils.ts
23
- const { default: AdminJS } = await dynamicImport('adminjs');
24
- const { Database, Resource } = await dynamicImport('@adminjs/prisma');
25
- const { AdminModule } = await dynamicImport('@adminjs/nestjs');
26
- const { default: importExportFeature } = await dynamicImport('@adminjs/import-export');
27
- const { default: passwordFeature } = await dynamicImport('@adminjs/passwords');
28
- const argon2 = await dynamicImport('argon2');
29
-
30
- // Below, this function in src/backoffice/utils.ts is used to get the resources you want to expose to AdminJS
31
- const resources = getAdminJSResources();
32
-
33
- // Once you create customized components, you can load them on the componentLoader just like the Dashboard component example
34
-
35
- const { componentLoader, Components } = await initializeComponents();
36
-
37
- // This gets the models from the Prisma schema, and then creates the AdminJS resources
38
- const schemaPath = './prisma/schema.prisma';
39
- const schema = readFileSync(schemaPath, 'utf-8');
40
- const dmmf = await getDMMF({ datamodel: schema });
41
-
42
- const models = [];
43
-
44
- for (const resource of resources) {
45
- const model = dmmf.datamodel.models.find(
46
- (model) => model.name === resource.name,
47
- );
48
-
49
- models.push({
50
- model,
51
- options: resource.options,
52
- features: model.name === 'User' ? [
53
- passwordFeature({
54
- properties: {
55
- encryptedPassword: 'password',
56
- password: 'plainPassword',
57
- },
58
- hash: argon2.hash,
59
- componentLoader,
60
- }),
61
- ] : [],
62
- });
63
- }
64
-
65
- AdminJS.registerAdapter({ Database, Resource });
66
-
67
-
68
- return AdminModule.createAdminAsync({
69
- imports: [PrismaModule],
70
- inject: [PrismaService],
71
- useFactory: async (prisma: PrismaService) => {
72
-
73
- // If you comment out this function below, you will be able to access the AdminJS with the DEFAULT_ADMIN credentials at the top of this file
74
- // With that, you can create a new user, and then undo the comment on this function to disable the default admin, using the new user you created
75
- const authenticate = async (email: string, password: string) => {
76
- const user = await prisma.user.findUnique({
77
- where: {
78
- email
79
- }
80
- });
81
-
82
- if (!user || user.role !== 'ADMIN') {
83
- return null;
84
- }
85
-
86
- const isPasswordValid = await argon2.verify(user.password, password);
87
-
88
- return isPasswordValid ? Promise.resolve({ email: user.email, role: user.role, id: user.id }) : null;
89
- }
90
-
91
- return {
92
- adminJsOptions: {
93
- rootPath: '/admin',
94
- // As you can see below, you can customize the dashboard component, which is the first page you see when you access the AdminJS
95
- dashboard: {
96
- component: Components.Dashboard,
97
- handler: async () => {
98
- return { some: 'output' };
99
- },
100
- },
101
- branding: {
102
- // Here you can change the company name, which appears on the browser's tab
103
- companyName: 'AppX Core Wizard',
104
- // This removes the "Made with love (...)" from the footer
105
- withMadeWithLove: false,
106
- // Here you can change the logo, currently using AppX's as an example
107
- logo: 'https://i.ibb.co/XZNRS5m/appxdigitalcom-logo.jpg',
108
- },
109
- resources: models.map((m) => {
110
- return {
111
- resource: { model: m.model, client: prisma },
112
- options: {
113
- ...m.options,
114
- actions: createActions(),
115
- },
116
- features: [...(m.features || []), importExportFeature({
117
- componentLoader
118
- })],
119
- };
120
- }),
121
-
122
- componentLoader,
123
- },
124
- auth: {
125
- authenticate,
126
- cookieName: process.env.SESSION_COOKIE_NAME,
127
- cookiePassword: process.env.SESSION_SECRET,
128
- },
129
- sessionOptions: {
130
- resave: false,
131
- saveUninitialized: true,
132
- secret: process.env.SESSION_SECRET,
133
- cookie: {
134
- httpOnly: process.env.NODE_ENV === 'production',
135
- //secure: process.env.NODE_ENV === 'production',
136
- secure: false,
137
- },
138
- name: process.env.SESSION_COOKIE_NAME,
139
- },
140
- };
141
- },
142
- });
143
- }
@@ -1,48 +0,0 @@
1
- import {
2
- MiddlewareConsumer,
3
- Module,
4
- NestModule,
5
- RequestMethod,
6
- } from '@nestjs/common';
7
- import { ConfigModule } from '@nestjs/config';
8
- import { APP_INTERCEPTOR } from '@nestjs/core';
9
- import { AppController } from './app.controller';
10
- import { AppService } from './app.service';
11
- import {AppxCoreModule, AuthModule, PrismaInterceptor} from '@appxdigital/appx-core';
12
- import {
13
- RequestContextModule,
14
- RequestContextMiddleware,
15
- } from 'nestjs-request-context';
16
- import { UserModule } from './modules/user/user.module';
17
- import { PermissionsConfig } from "./config/permissions.config";
18
- import { createAdminJsModule } from "./backoffice/admin";
19
-
20
- @Module({
21
- imports: [
22
- RequestContextModule,
23
- ConfigModule.forRoot({
24
- isGlobal: true,
25
- expandVariables: true,
26
- envFilePath: `.env.${process.env.NODE_ENV || 'development'}`,
27
- }),
28
- AppxCoreModule.forRoot(PermissionsConfig),
29
- AuthModule,
30
- UserModule,
31
- createAdminJsModule().then((AdminJsModule) => AdminJsModule),
32
- ],
33
- controllers: [AppController],
34
- providers: [
35
- AppService,
36
- {
37
- provide: APP_INTERCEPTOR,
38
- useClass: PrismaInterceptor,
39
- },
40
- ],
41
- })
42
- export class AppModule implements NestModule {
43
- configure(consumer: MiddlewareConsumer) {
44
- consumer
45
- .apply(RequestContextMiddleware)
46
- .forRoutes({ path: '*', method: RequestMethod.ALL });
47
- }
48
- }
@@ -1,14 +0,0 @@
1
- import { dynamicImport } from "./utils";
2
-
3
- async function loadComponents() {
4
- const { ComponentLoader } = await dynamicImport('adminjs');
5
- const componentLoader = new ComponentLoader();
6
-
7
- const Components = {
8
- Dashboard: componentLoader.add('dashboard', './components/dashboard'),
9
- };
10
-
11
- return { componentLoader, Components };
12
- }
13
-
14
- export const initializeComponents = loadComponents;
@@ -1,131 +0,0 @@
1
- import { PermissionsConfig } from "../config/permissions.config";
2
-
3
- export const dynamicImport = async (packageName: string) =>
4
- new Function(`return import('${packageName}')`)();
5
-
6
- export const getAdminJSResources = (specific = null) => {
7
- const resources = [
8
- {
9
- name: 'User',
10
- options: {},
11
- },
12
- ];
13
-
14
- return specific ? resources.filter((r) => r.name === specific) : resources;
15
- };
16
-
17
- export function createPermissionHandler(role: string, resource: string, action: string) {
18
- const rolePermissions = PermissionsConfig[resource]?.[role];
19
-
20
- if (!rolePermissions) {
21
- return () => false;
22
- }
23
-
24
- const actionMapping = {
25
- list: 'findMany',
26
- show: 'findUnique',
27
- edit: 'update',
28
- delete: 'delete',
29
- new: 'create',
30
- };
31
-
32
- const mappedAction = actionMapping[action];
33
-
34
- if (!mappedAction || !rolePermissions[mappedAction]) {
35
- return () => false;
36
- }
37
-
38
- if (rolePermissions[mappedAction] === 'ALL') {
39
- return () => true;
40
- }
41
-
42
- if (typeof rolePermissions[mappedAction] === 'object') {
43
-
44
- return (requestContext) => {
45
- // @ts-ignore
46
- const clauses = rolePermissions[mappedAction]?.clauses;
47
- if (!clauses) return () => false;
48
- return parseClauses(clauses)(requestContext);
49
- };
50
- }
51
-
52
- return () => false;
53
- }
54
-
55
- const clauseMapping = {
56
- '$USER_ID': "id",
57
- '$USER_EMAIL': "email",
58
- };
59
-
60
- const parseClauses = (clauses) => {
61
- return (requestContext) => {
62
- if (!clauses || !Array.isArray(clauses)) return true;
63
-
64
- for (const clause of clauses) {
65
- if (clause.type === 'field') {
66
- for (const [field, value] of Object.entries(clause.conditions)) {
67
- let actualValue = value;
68
-
69
- if (typeof value === 'string' && value.startsWith('$')) {
70
- actualValue = requestContext?.currentAdmin[clauseMapping[value]] ?? null;
71
- }
72
-
73
- if (requestContext.record?.param(field) !== actualValue) {
74
- return false;
75
- }
76
- }
77
- }
78
- }
79
- return true;
80
- };
81
- };
82
-
83
-
84
- export const addBasicFilters = (filters) => {
85
- return async (request, context) => {
86
- for (const [field, value] of Object.entries(filters)) {
87
- request.query[`filters.${field}`] = value;
88
- }
89
- return request;
90
- };
91
- };
92
-
93
- export const createActions = () => {
94
- return {
95
- list: {
96
- isAccessible: createIsAccessible('list'),
97
- },
98
- show: {
99
- isAccessible: createIsAccessible('show'),
100
- },
101
- edit: {
102
- isAccessible: createIsAccessible('edit'),
103
- },
104
- delete: {
105
- isAccessible: createIsAccessible('delete'),
106
- },
107
- new: {
108
- isAccessible: createIsAccessible('new'),
109
- },
110
- };
111
- };
112
-
113
- const createIsAccessible = (action) => {
114
- return (context) => {
115
- if (!context.currentAdmin) return false;
116
- const { role } = context.currentAdmin;
117
- return createPermissionHandler(role, context.resource.model.name, action)(context);
118
- };
119
- };
120
-
121
- const createBeforeHook = (filters) => {
122
- return (request, context) => {
123
- if (context.currentAdmin) {
124
- request.query.filters = {
125
- ...request.query.filters,
126
- ...filters,
127
- };
128
- }
129
- return request;
130
- };
131
- };