@appxdigital/appx-core-cli 1.0.11 → 1.0.13
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/cli.js +578 -0
- package/package.json +5 -4
- package/scaffold/.env.template +33 -0
- package/scaffold/.eslintrc.js +26 -0
- package/scaffold/.gitattributes +2 -0
- package/scaffold/.prettierrc +9 -0
- package/scaffold/prisma/schema.prisma.template +47 -0
- package/scaffold/src/app.controller.ts +13 -0
- package/{templates/app.module.template.js → scaffold/src/app.module.ts} +8 -4
- package/scaffold/src/app.service.ts +8 -0
- package/{templates/adminjs/dashboard.template.js → scaffold/src/backoffice/components/dashboard.js} +0 -2
- package/{templates/permissions.config.template.js → scaffold/src/config/permissions.config.ts} +1 -4
- package/{templates/bootstrap.template.js → scaffold/src/main.ts} +9 -10
- package/{templates/prisma.module.template.js → scaffold/src/prisma/prisma.module.ts} +3 -3
- package/utils/fileUploadConfig.js +11 -11
- package/README.md +0 -159
- package/utils/dependency-versions.json +0 -28
- package/wizard.js +0 -599
- /package/{templates/admin.config.template.js → scaffold/src/config/admin.config.ts} +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
datasource db {
|
|
2
|
+
provider = "{{DB_PROVIDER}}"
|
|
3
|
+
url = env("DB_URL")
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
generator client {
|
|
7
|
+
provider = "prisma-client-js"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
generator nestgraphql {
|
|
11
|
+
provider = "node node_modules/prisma-nestjs-graphql"
|
|
12
|
+
output = "../src/generated/"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
model User {
|
|
16
|
+
id Int @id @default(autoincrement())
|
|
17
|
+
email String @unique
|
|
18
|
+
name String?
|
|
19
|
+
password String? /// @Role(none)
|
|
20
|
+
role Role @default(GUEST)
|
|
21
|
+
refreshTokens UserRefreshToken[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
model Session {
|
|
25
|
+
id String @id
|
|
26
|
+
sid String @unique
|
|
27
|
+
data String @db.Text
|
|
28
|
+
expiresAt DateTime
|
|
29
|
+
userId Int?
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
model UserRefreshToken {
|
|
33
|
+
id String @id @default(cuid())
|
|
34
|
+
token String @unique @db.VarChar(255)
|
|
35
|
+
userId Int
|
|
36
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
37
|
+
expiresAt DateTime
|
|
38
|
+
createdAt DateTime @default(now())
|
|
39
|
+
revokedAt DateTime?
|
|
40
|
+
|
|
41
|
+
@@index([userId])
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
enum Role {
|
|
45
|
+
ADMIN
|
|
46
|
+
GUEST
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {Controller, Get} from '@nestjs/common';
|
|
2
|
+
import {AppService} from './app.service';
|
|
3
|
+
|
|
4
|
+
@Controller()
|
|
5
|
+
export class AppController {
|
|
6
|
+
constructor(private readonly appService: AppService) {
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@Get()
|
|
10
|
+
getHello(): string {
|
|
11
|
+
return this.appService.getHello();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -2,9 +2,9 @@ import {MiddlewareConsumer, Module, NestModule, RequestMethod} from '@nestjs/com
|
|
|
2
2
|
import {AppController} from './app.controller';
|
|
3
3
|
import {AppService} from './app.service';
|
|
4
4
|
import {ConfigModule} from '@nestjs/config';
|
|
5
|
-
import {AuthModule, PrismaInterceptor,
|
|
6
|
-
import {APP_INTERCEPTOR} from '@nestjs/core';
|
|
7
|
-
import {
|
|
5
|
+
import {AppxCoreAdminModule, AppxCoreModule, AuthModule, PrismaInterceptor, UserPopulationGuard} from '@appxdigital/appx-core';
|
|
6
|
+
import {APP_GUARD, APP_INTERCEPTOR} from '@nestjs/core';
|
|
7
|
+
import {RequestContextMiddleware, RequestContextModule} from 'nestjs-request-context'
|
|
8
8
|
import {PermissionsConfig} from './config/permissions.config';
|
|
9
9
|
import {AdminConfig} from './config/admin.config';
|
|
10
10
|
|
|
@@ -28,10 +28,14 @@ import {AdminConfig} from './config/admin.config';
|
|
|
28
28
|
provide: APP_INTERCEPTOR,
|
|
29
29
|
useClass: PrismaInterceptor,
|
|
30
30
|
},
|
|
31
|
+
{
|
|
32
|
+
provide: APP_GUARD,
|
|
33
|
+
useClass: UserPopulationGuard,
|
|
34
|
+
},
|
|
31
35
|
],
|
|
32
36
|
})
|
|
33
37
|
export class AppModule implements NestModule {
|
|
34
|
-
configure
|
|
38
|
+
configure(consumer: MiddlewareConsumer) {
|
|
35
39
|
consumer
|
|
36
40
|
.apply(RequestContextMiddleware)
|
|
37
41
|
.forRoutes({path: '*', method: RequestMethod.ALL});
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {PrismaService} from '@appxdigital/appx-core';
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import * as morgan from 'morgan';
|
|
1
|
+
import {NestFactory} from '@nestjs/core';
|
|
2
|
+
import {AppModule} from './app.module';
|
|
3
|
+
import {ConfigService} from '@nestjs/config';
|
|
4
|
+
import session from 'express-session';
|
|
5
|
+
import passport from 'passport';
|
|
6
|
+
import {CorePrismaSessionStore, PrismaService} from '@appxdigital/appx-core';
|
|
7
|
+
import {ValidationPipe} from '@nestjs/common';
|
|
8
|
+
import morgan from 'morgan';
|
|
10
9
|
|
|
11
10
|
async function bootstrap() {
|
|
12
11
|
const app = await NestFactory.create(AppModule);
|
|
@@ -41,7 +40,7 @@ async function bootstrap() {
|
|
|
41
40
|
origin: 'http://localhost:3000',
|
|
42
41
|
credentials: true,
|
|
43
42
|
});
|
|
44
|
-
app.useGlobalPipes(new ValidationPipe({
|
|
43
|
+
app.useGlobalPipes(new ValidationPipe({transform: true}));
|
|
45
44
|
await app.listen(port);
|
|
46
45
|
console.log(`Your application is successfully running on: http://www.localhost:${port} 🚀`);
|
|
47
46
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import {Global, Module} from '@nestjs/common';
|
|
2
|
+
import {PrismaClient} from '@prisma/client';
|
|
3
3
|
import {PERMISSIONS_CONFIG_TOKEN, PermissionsConfigType, PrismaService} from '@appxdigital/appx-core';
|
|
4
|
-
import {
|
|
4
|
+
import {PermissionsConfig} from '../config/permissions.config';
|
|
5
5
|
|
|
6
6
|
@Global()
|
|
7
7
|
@Module({
|
|
@@ -35,7 +35,7 @@ function getEnvVariables(provider, providerOptions) {
|
|
|
35
35
|
export async function gatherFileUploadSettings() {
|
|
36
36
|
const config = {};
|
|
37
37
|
|
|
38
|
-
const {
|
|
38
|
+
const {provider} = await inquirer.prompt([
|
|
39
39
|
{
|
|
40
40
|
type: 'list',
|
|
41
41
|
name: 'provider',
|
|
@@ -47,20 +47,20 @@ export async function gatherFileUploadSettings() {
|
|
|
47
47
|
|
|
48
48
|
if (provider === 'aws') {
|
|
49
49
|
config.providerOptions = await inquirer.prompt([
|
|
50
|
-
{
|
|
51
|
-
{
|
|
52
|
-
{
|
|
53
|
-
{
|
|
50
|
+
{type: 'input', name: 'bucket', message: 'Enter AWS S3 bucket name:'},
|
|
51
|
+
{type: 'input', name: 'region', message: 'Enter AWS region:'},
|
|
52
|
+
{type: 'input', name: 'accessKeyId', message: 'Enter AWS access key ID:'},
|
|
53
|
+
{type: 'input', name: 'secretAccessKey', message: 'Enter AWS secret access key:'},
|
|
54
54
|
]);
|
|
55
55
|
} else if (provider === 'gcp') {
|
|
56
56
|
config.providerOptions = await inquirer.prompt([
|
|
57
|
-
{
|
|
58
|
-
{
|
|
59
|
-
{
|
|
57
|
+
{type: 'input', name: 'bucket', message: 'Enter GCP bucket name:'},
|
|
58
|
+
{type: 'input', name: 'projectId', message: 'Enter GCP project ID:'},
|
|
59
|
+
{type: 'input', name: 'keyFilePath', message: 'Enter the path to the GCP service account key file:'},
|
|
60
60
|
]);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
const {
|
|
63
|
+
const {endpoint, maxSize, fileType, multiple} = await inquirer.prompt([
|
|
64
64
|
{
|
|
65
65
|
type: 'input',
|
|
66
66
|
name: 'endpoint',
|
|
@@ -96,7 +96,7 @@ export async function gatherFileUploadSettings() {
|
|
|
96
96
|
config.multiple = multiple;
|
|
97
97
|
|
|
98
98
|
if (fileType in MimeTypes) {
|
|
99
|
-
const {
|
|
99
|
+
const {selectedMimeTypes} = await inquirer.prompt([
|
|
100
100
|
{
|
|
101
101
|
type: 'checkbox',
|
|
102
102
|
name: 'selectedMimeTypes',
|
|
@@ -108,7 +108,7 @@ export async function gatherFileUploadSettings() {
|
|
|
108
108
|
config.allowedTypes = selectedMimeTypes;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const {
|
|
111
|
+
const {roles} = await inquirer.prompt([
|
|
112
112
|
{
|
|
113
113
|
type: 'input',
|
|
114
114
|
name: 'roles',
|
package/README.md
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<a href="https://appx-digital.com/" target="_blank">
|
|
3
|
-
<img src="./assets/logo_appx.svg" width="300" alt="Appx Logo" />
|
|
4
|
-
</a>
|
|
5
|
-
</p>
|
|
6
|
-
|
|
7
|
-
## Description
|
|
8
|
-
|
|
9
|
-
<p style="text-align: left;">
|
|
10
|
-
This project is a foundational Node.js boilerplate built with <a href="https://nestjs.com/" target="_blank">NestJS</a>, <a href="https://www.prisma.io/" target="_blank">Prisma ORM</a>, and <a href="https://graphql.org/" target="_blank">GraphQL</a>. It simplifies the development process by using Prisma ORM as the single source of truth, automatically generating and managing all essential components of your application.
|
|
11
|
-
</p>
|
|
12
|
-
|
|
13
|
-
## Installation
|
|
14
|
-
|
|
15
|
-
1. Open your terminal and run the following command:
|
|
16
|
-
```bash
|
|
17
|
-
npm install @appxdigital/appx-core-cli -g
|
|
18
|
-
appx-core create
|
|
19
|
-
```
|
|
20
|
-
2. Follow the prompts provided by the setup wizard. This will scaffold a new NestJS project with `appx-core` pre-integrated.
|
|
21
|
-
3. Open created project in your favorite IDE.
|
|
22
|
-
|
|
23
|
-
## Configure Database Schema
|
|
24
|
-
|
|
25
|
-
1. Open the `prisma/schema.prisma` file.
|
|
26
|
-
2. Modify this file to define your application's specific data models. You can add new models, fields, and relations as needed.
|
|
27
|
-
|
|
28
|
-
Note: If you already have a database created and want to import the structure, use the following command:
|
|
29
|
-
|
|
30
|
-
```
|
|
31
|
-
npx prisma db pull
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**This command will override the schema.prisma file, please ensure that the original configurations and models are kept (you can adapt User and Session model, but it's advised to keep all default attributes)**
|
|
35
|
-
|
|
36
|
-
## Generate Modules and Prisma Client
|
|
37
|
-
|
|
38
|
-
1. After editing your schema, generate the modules for each model by running:
|
|
39
|
-
```bash
|
|
40
|
-
appx-core generate
|
|
41
|
-
```
|
|
42
|
-
2. This command also updates the `@prisma/client` library within your `node_modules` to match your defined schema, providing type-safe database access. This command should be run everytime you make a change to your prisma schema.
|
|
43
|
-
|
|
44
|
-
## Apply Database Migrations
|
|
45
|
-
|
|
46
|
-
1. To create the necessary database tables and apply your schema changes, run:
|
|
47
|
-
```bash
|
|
48
|
-
npx prisma migrate dev
|
|
49
|
-
```
|
|
50
|
-
2. This will:
|
|
51
|
-
* Create a new SQL migration file reflecting the changes between your current schema and the last migration.
|
|
52
|
-
* Apply the migration to your database.
|
|
53
|
-
|
|
54
|
-
## Run the Application
|
|
55
|
-
|
|
56
|
-
1. Start your application in development mode using:
|
|
57
|
-
```bash
|
|
58
|
-
npm run start:dev
|
|
59
|
-
```
|
|
60
|
-
2. The application will start on `http://localhost:3000` or the port specified during your setup, you can change this in the `.env` file.
|
|
61
|
-
|
|
62
|
-
## Register Your First User
|
|
63
|
-
|
|
64
|
-
1. To create a user account, send an HTTP `POST` request to the `/auth/register` endpoint.
|
|
65
|
-
2. The request body must be JSON and include the `email` and `password`:
|
|
66
|
-
```json
|
|
67
|
-
{
|
|
68
|
-
"email": "email@example.com",
|
|
69
|
-
"password": "password"
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
3. On success, you'll receive a JSON response confirming registration and containing the new user's details.
|
|
73
|
-
|
|
74
|
-
## Log In
|
|
75
|
-
|
|
76
|
-
1. To log in with the registered user, send an HTTP `POST` request to the `/auth/login` endpoint.
|
|
77
|
-
2. The request body must be JSON and include the same fields used for registration:
|
|
78
|
-
```json
|
|
79
|
-
{
|
|
80
|
-
"email": "email@example.com",
|
|
81
|
-
"password": "password"
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
3. On success, you'll receive a JSON response confirming login and containing the user's details.
|
|
85
|
-
|
|
86
|
-
## Permissions
|
|
87
|
-
|
|
88
|
-
This project uses RBAC to control user access. Permissions are defined in a configuration file and checked automatically before controller actions and during database queries.
|
|
89
|
-
|
|
90
|
-
### Configuration
|
|
91
|
-
|
|
92
|
-
* Permissions are defined in the `src/config/permissions.config.ts` file.
|
|
93
|
-
* The configuration structure is hierarchical: `ModelName` -> `RoleName` -> `ActionName` -> `Rule`.
|
|
94
|
-
|
|
95
|
-
```typescript
|
|
96
|
-
export const PermissionsConfig: PermissionsConfigType = {
|
|
97
|
-
ModelName: { // e.g., 'User'; links to controller's entityName
|
|
98
|
-
RoleName: { // e.g., 'ADMIN', 'CLIENT'; matches user roles
|
|
99
|
-
ActionName: Rule, // e.g., 'findUnique', 'update'
|
|
100
|
-
// ... other actions for this role/model
|
|
101
|
-
},
|
|
102
|
-
// ... other roles for this model
|
|
103
|
-
},
|
|
104
|
-
// ... other models
|
|
105
|
-
};
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Permission Rules & Effects
|
|
109
|
-
|
|
110
|
-
The `Rule` assigned to an action determines if a user with that role can perform the action:
|
|
111
|
-
|
|
112
|
-
* **`'ALL'`:** Grants unrestricted access for the specific action.
|
|
113
|
-
* **Object `{ conditions: { ... } }`:** Grants access *only if* the specified conditions match the data being accessed.
|
|
114
|
-
* These `conditions` (e.g., `{ id: '$USER_ID' }`) are automatically added as `WHERE` clauses to the underlying database query by the `PrismaService`.
|
|
115
|
-
* Placeholders like `$USER_ID` are replaced with the current user's actual data (e.g., `req.user.id`).
|
|
116
|
-
* **Missing Rule:** If no rule (`'ALL'` or an object) is defined for an action under the user's role, access is **denied**.
|
|
117
|
-
|
|
118
|
-
### Applying Permissions
|
|
119
|
-
|
|
120
|
-
* Protect controller methods by decorating them with `@Permission('actionName')`. This links the method to an `ActionName` in your `permissions.config.ts`.
|
|
121
|
-
* Methods *without* the `@Permission` decorator are not subject to specific action checks by the `RbacGuard` and are allowed access.
|
|
122
|
-
|
|
123
|
-
```typescript
|
|
124
|
-
@Controller('users')
|
|
125
|
-
export class UserController extends CoreController<User> {
|
|
126
|
-
|
|
127
|
-
@Get(':id')
|
|
128
|
-
@Permission('findUnique') // Checks config for User -> Role -> findUnique
|
|
129
|
-
async findOne(@Param('id') id: string) {
|
|
130
|
-
/* ... */
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
@Get('/public/info')
|
|
134
|
-
// No @Permission decorator - RbacGuard allows by default access for this route
|
|
135
|
-
async getPublicInfo() {
|
|
136
|
-
/* ... */
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
static get entityName(): string {
|
|
140
|
-
return 'User';
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Adding Permissions for a New Action
|
|
146
|
-
|
|
147
|
-
1. **Decorate Method & Choose Name:** Add the `@Permission('yourActionName')` decorator to the relevant controller method, choosing a unique string for `'yourActionName'` (e.g., `'publishItem'`, `'resetPassword'`).
|
|
148
|
-
|
|
149
|
-
2. **Update Config:** Edit `permissions.config.ts`. Add the `actionName` under the appropriate `ModelName` -> `RoleName`(s). Set the rule:
|
|
150
|
-
* Use `'ALL'` for full access.
|
|
151
|
-
* Use an object like `{ conditions: { field: '$USER_ID' } }` for conditional access (enforced by the database query).
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// Example: Adding 'publishItem' rule to Article model config
|
|
155
|
-
Article: {
|
|
156
|
-
ADMIN: { /*...,*/ publishItem: 'ALL' },
|
|
157
|
-
EDITOR: { /*...,*/ publishItem: { conditions: { authorId: '$USER_ID' } } },
|
|
158
|
-
},
|
|
159
|
-
```
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"dependencies": {
|
|
3
|
-
"@prisma/client": "^6.5.0",
|
|
4
|
-
"prisma-nestjs-graphql": "20.0.3",
|
|
5
|
-
"prisma-case-format": "2.2.1",
|
|
6
|
-
"class-validator": "0.14.1",
|
|
7
|
-
"class-transformer": "0.5.1",
|
|
8
|
-
"nestjs-request-context": "4.0.0",
|
|
9
|
-
"passport": "0.7.0",
|
|
10
|
-
"passport-local": "1.0.0",
|
|
11
|
-
"express-session": "1.18.1",
|
|
12
|
-
"express": "4.21.1",
|
|
13
|
-
"argon2": "0.41.1",
|
|
14
|
-
"rxjs": "7.8.1",
|
|
15
|
-
"reflect-metadata": "0.2.0",
|
|
16
|
-
"morgan": "^1.10.1"
|
|
17
|
-
},
|
|
18
|
-
"devDependencies": {
|
|
19
|
-
"@types/express": "5.0.0",
|
|
20
|
-
"prisma": "5.22.0",
|
|
21
|
-
"@types/express-formidable": "1.2.3",
|
|
22
|
-
"@types/inquirer": "9.0.7",
|
|
23
|
-
"@types/multer": "1.4.12",
|
|
24
|
-
"@types/passport-local": "1.0.38",
|
|
25
|
-
"typescript": "5.1.3",
|
|
26
|
-
"@types/morgan": "^1.9.10"
|
|
27
|
-
}
|
|
28
|
-
}
|