diff --git a/.gitignore b/.gitignore index da9fc4e..0ffaac4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json -node_modules + +# Env files +**/.env +**/.env.* +!**/.env.example \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 43ee4f6..ece7630 100644 --- a/.prettierrc +++ b/.prettierrc @@ -4,6 +4,5 @@ "semi": true, "tabWidth": 4, "printWidth": 150, - "bracketSpacing": true, - "endOfLine": "auto" + "bracketSpacing": true } diff --git a/.vscode/settings.json b/.vscode/settings.json index caff6cb..2b07ebc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,5 +10,5 @@ "**/node_modules": true }, "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", } \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..8f4ccd2 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,7 @@ +DATABASE_PASSWORD=postgres +DATABASE_NAME=bot_db +DATABASE_USER=postgres +DATABASE_HOST=localhost +DATABASE_PORT=5432 + +SERVER_PORT=3000 \ No newline at end of file diff --git a/backend/config/index.ts b/backend/config/index.ts new file mode 100644 index 0000000..280614f --- /dev/null +++ b/backend/config/index.ts @@ -0,0 +1,20 @@ +import { config as configInit } from 'dotenv'; + +configInit(); + +export const config = { + database: { + type: 'postgres', + host: process.env.DATABASE_HOST || 'localhost', + port: +process.env.DATABASE_PORT || 5432, + username: process.env.DATABASE_USERNAME || 'postgres', + password: process.env.DATABASE_PASSWORD || '', + database: process.env.DATABASE_DB || 'bot_db', + synchronize: true, + logging: false, + autoLoadEntities: true, + }, + server: { + port: +process.env.SERVER_PORT || 8080, + }, +}; diff --git a/backend/libs/database/user.entity.ts b/backend/libs/database/user.entity.ts new file mode 100644 index 0000000..48317eb --- /dev/null +++ b/backend/libs/database/user.entity.ts @@ -0,0 +1,14 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity() +export class User { + constructor(props?: Partial) { + Object.assign(this, props); + } + + @PrimaryColumn() + public id!: string; + + @Column({ nullable: true }) + public user_name: string; +} diff --git a/backend/libs/libs.module.ts b/backend/libs/libs.module.ts new file mode 100644 index 0000000..6761b66 --- /dev/null +++ b/backend/libs/libs.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from './database/user.entity'; +@Module({ + imports: [TypeOrmModule.forFeature([User])], + exports: [TypeOrmModule], +}) +export class LibsModule {} diff --git a/backend/package.json b/backend/package.json index 3695b91..c348abc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,7 +23,9 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", - "nestjs": "^0.0.1", + "@nestjs/swagger": "^7.1.16", + "@nestjs/typeorm": "^10.0.1", + "dotenv": "^16.3.1", "pg": "^8.11.3", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml index 6315ae2..27ff489 100644 --- a/backend/pnpm-lock.yaml +++ b/backend/pnpm-lock.yaml @@ -14,9 +14,15 @@ dependencies: '@nestjs/platform-express': specifier: ^10.0.0 version: 10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8) - nestjs: - specifier: ^0.0.1 - version: 0.0.1 + '@nestjs/swagger': + specifier: ^7.1.16 + version: 7.1.16(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13) + '@nestjs/typeorm': + specifier: ^10.0.1 + version: 10.0.1(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17) + dotenv: + specifier: ^16.3.1 + version: 16.3.1 pg: specifier: ^8.11.3 version: 8.11.3 @@ -941,6 +947,23 @@ packages: transitivePeerDependencies: - encoding + /@nestjs/mapped-types@2.0.3(@nestjs/common@10.2.8)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + reflect-metadata: 0.1.13 + dev: false + /@nestjs/platform-express@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8): resolution: {integrity: sha512-WoSSVtwIRc5AdGMHWVzWZK4JZLT0f4o2xW8P9gQvcX+omL8W1kXCfY8GQYXNBG84XmBNYH8r0FtC8oMe/lH5NQ==} peerDependencies: @@ -972,6 +995,33 @@ packages: - chokidar dev: true + /@nestjs/swagger@7.1.16(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13): + resolution: {integrity: sha512-f9KBk/BX9MUKPTj7tQNYJ124wV/jP5W2lwWHLGwe/4qQXixuDOo39zP55HIJ44LE7S04B7BOeUOo9GBJD/vRcw==} + peerDependencies: + '@fastify/static': ^6.0.0 + '@nestjs/common': ^9.0.0 || ^10.0.0 + '@nestjs/core': ^9.0.0 || ^10.0.0 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + dependencies: + '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.3(@nestjs/common@10.2.8)(reflect-metadata@0.1.13) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 3.2.0 + reflect-metadata: 0.1.13 + swagger-ui-dist: 5.9.1 + dev: false + /@nestjs/testing@10.2.8(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(@nestjs/platform-express@10.2.8): resolution: {integrity: sha512-9Kj5IQhM67/nj/MT6Wi2OmWr5YQnCMptwKVFrX1TDaikpY12196v7frk0jVjdT7wms7rV07GZle9I2z0aSjqtQ==} peerDependencies: @@ -991,6 +1041,23 @@ packages: tslib: 2.6.2 dev: true + /@nestjs/typeorm@10.0.1(@nestjs/common@10.2.8)(@nestjs/core@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17): + resolution: {integrity: sha512-YVFYL7D25VAVp5/G+KLXIgsRfYomA+VaFZBpm2rtwrrBOmkXNrxr7kuI2bBBO/Xy4kKBDe6wbvIVVFeEA7/ngA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0 + reflect-metadata: ^0.1.13 + rxjs: ^7.2.0 + typeorm: ^0.3.0 + dependencies: + '@nestjs/common': 10.2.8(reflect-metadata@0.1.13)(rxjs@7.8.1) + '@nestjs/core': 10.2.8(@nestjs/common@10.2.8)(@nestjs/platform-express@10.2.8)(reflect-metadata@0.1.13)(rxjs@7.8.1) + reflect-metadata: 0.1.13 + rxjs: 7.8.1 + typeorm: 0.3.17(pg@8.11.3)(ts-node@10.9.1) + uuid: 9.0.1 + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1653,7 +1720,6 @@ packages: /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -3764,7 +3830,6 @@ packages: hasBin: true dependencies: argparse: 2.0.1 - dev: true /jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} @@ -3867,7 +3932,6 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -5006,6 +5070,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /swagger-ui-dist@5.9.1: + resolution: {integrity: sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==} + dev: false + /symbol-observable@4.0.0: resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} engines: {node: '>=0.10'} diff --git a/backend/src/app.controller.spec.ts b/backend/src/app.controller.spec.ts deleted file mode 100644 index 2552ec5..0000000 --- a/backend/src/app.controller.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -describe('AppController', () => { - let appController: AppController; - - beforeEach(async () => { - const app: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [AppService], - }).compile(); - - appController = app.get(AppController); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts deleted file mode 100644 index a325e8b..0000000 --- a/backend/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index da156b4..52a881e 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -1,10 +1,13 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { config } from 'config'; +import { LibsModule } from 'libs/libs.module'; +import { AppInitService } from './modules/initialization/app.init.service'; +import { UserModule } from './modules/user/user.module'; @Module({ - imports: [], - controllers: [AppController], - providers: [AppService], + imports: [LibsModule, UserModule, TypeOrmModule.forRoot(config.database)], + controllers: [], + providers: [AppInitService], }) export class AppModule {} diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts deleted file mode 100644 index 61b7a5b..0000000 --- a/backend/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/backend/src/main.ts b/backend/src/main.ts index 827b631..282ef4d 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,8 +1,14 @@ +import { Logger } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { config } from 'config'; import { AppModule } from './app.module'; +import { swagger } from './swagger'; async function bootstrap() { - const app = await NestFactory.create(AppModule); - await app.listen(3000); + const app = await NestFactory.create(AppModule, { + logger: ['log', 'debug', 'error', 'warn', 'verbose'], + }); + swagger(app); + await app.listen(config.server.port, () => Logger.log(`Server started on port ${config.server.port}`)); } bootstrap(); diff --git a/backend/src/modules/initialization/app.init.service.ts b/backend/src/modules/initialization/app.init.service.ts new file mode 100644 index 0000000..e22de72 --- /dev/null +++ b/backend/src/modules/initialization/app.init.service.ts @@ -0,0 +1,11 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from 'libs/database/user.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class AppInitService implements OnModuleInit { + constructor(@InjectRepository(User) private userRepository: Repository) {} + + async onModuleInit() {} +} diff --git a/backend/src/modules/user/user.controller.ts b/backend/src/modules/user/user.controller.ts new file mode 100644 index 0000000..8305e4a --- /dev/null +++ b/backend/src/modules/user/user.controller.ts @@ -0,0 +1,18 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { IGetUser } from './user.dto'; +import { UserService } from './user.service'; + +@ApiTags('User') +@Controller('user') +export class UserController { + constructor(private adminService: UserService) {} + + @ApiOperation({ + description: 'Create or get user from db', + }) + @Post('get') + async getUser(@Body() data: IGetUser) { + return await this.adminService.getUser(data); + } +} diff --git a/backend/src/modules/user/user.dto.ts b/backend/src/modules/user/user.dto.ts new file mode 100644 index 0000000..b6a44b9 --- /dev/null +++ b/backend/src/modules/user/user.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class IGetUser { + @ApiProperty({ description: 'telegram id', example: '123456' }) readonly id: string; + @ApiProperty({ description: 'telegram username', example: 'durov' }) readonly username?: string; +} diff --git a/backend/src/modules/user/user.module.ts b/backend/src/modules/user/user.module.ts new file mode 100644 index 0000000..45f7945 --- /dev/null +++ b/backend/src/modules/user/user.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { LibsModule } from 'libs/libs.module'; +import { UserController } from './user.controller'; +import { UserService } from './user.service'; + +@Module({ + imports: [LibsModule], + controllers: [UserController], + providers: [UserService], +}) +export class UserModule {} diff --git a/backend/src/modules/user/user.service.ts b/backend/src/modules/user/user.service.ts new file mode 100644 index 0000000..c334eb9 --- /dev/null +++ b/backend/src/modules/user/user.service.ts @@ -0,0 +1,27 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { User } from 'libs/database/user.entity'; +import { Repository } from 'typeorm'; +import { IGetUser } from './user.dto'; + +@Injectable() +export class UserService { + private readonly logger: Logger = new Logger(UserService.name); + constructor(@InjectRepository(User) private userRepository: Repository) {} + + async getUser(data: IGetUser) { + try { + this.logger.debug(`[admin.getUser] data: ${JSON.stringify(data)}`); + let user = await this.userRepository.findOne({ + where: { id: data.id }, + }); + if (!user) { + user = await this.userRepository.save({ id: data.id, user_name: data.username }); + this.logger.log(`User ${data.id} created`); + } + return user; + } catch (error) { + this.logger.log(`[getUser] ${JSON.stringify({ error })}`); + } + } +} diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts new file mode 100644 index 0000000..b206b2b --- /dev/null +++ b/backend/src/swagger.ts @@ -0,0 +1,9 @@ +import { INestApplication } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; + +export function swagger(app: INestApplication): INestApplication { + const config = new DocumentBuilder().setTitle('Neuro website').setDescription('Some description').setVersion('0.1').build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api', app, document); + return app; +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 95f5641..000fbb1 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -18,4 +18,4 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false } -} +} \ No newline at end of file