diff --git a/.gitignore b/.gitignore index 40b878d..39f044d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -node_modules/ \ No newline at end of file +node_modules/ +.env +data/ +dist/ \ No newline at end of file diff --git a/dev.compose.yml b/dev.compose.yml index 0d122cc..e70275f 100644 --- a/dev.compose.yml +++ b/dev.compose.yml @@ -30,6 +30,8 @@ services: labels: - "traefik.http.routers.backend.rule=Host(`api.cashlow.local`)" - "traefik.http.services.backend.loadbalancer.server.port=3000" + env_file: + - .env frontend: tty: true @@ -46,6 +48,21 @@ services: - "traefik.http.routers.frontend.rule=Host(`app.cashlow.local`)" - "traefik.http.services.frontend.loadbalancer.server.port=3000" + database: + tty: true + restart: unless-stopped + image: timescale/timescaledb-ha:pg17 + volumes: + - ./data/postgres:/home/postgres/pgdata/data + env_file: + - .env + user: root + ports: + - "5432:5432" + networks: + cashlow: + ipv4_address: 10.231.215.4 + networks: cashlow: driver: bridge diff --git a/docs/yaak/yaak.ev_FHr7MPpAvE.yaml b/docs/yaak/yaak.ev_FHr7MPpAvE.yaml new file mode 100644 index 0000000..1fac24f --- /dev/null +++ b/docs/yaak/yaak.ev_FHr7MPpAvE.yaml @@ -0,0 +1,17 @@ +type: environment +model: environment +id: ev_FHr7MPpAvE +workspaceId: wk_yK68KSnsqe +createdAt: 2025-12-22T17:05:40.703553024 +updatedAt: 2025-12-22T17:06:07.650085870 +name: Global Variables +public: true +base: true +parentModel: workspace +parentId: null +variables: +- enabled: true + name: BASE_URL + value: http://api.cashlow.local + id: Zm8JuTSFX6 +color: null diff --git a/docs/yaak/yaak.fl_kD6z8aiTcL.yaml b/docs/yaak/yaak.fl_kD6z8aiTcL.yaml new file mode 100644 index 0000000..7d650f5 --- /dev/null +++ b/docs/yaak/yaak.fl_kD6z8aiTcL.yaml @@ -0,0 +1,13 @@ +type: folder +model: folder +id: fl_kD6z8aiTcL +createdAt: 2025-12-22T21:56:20.134411915 +updatedAt: 2025-12-22T21:56:20.134415476 +workspaceId: wk_yK68KSnsqe +folderId: null +authentication: {} +authenticationType: null +description: '' +headers: [] +name: /users +sortPriority: -1766440600000.0 diff --git a/docs/yaak/yaak.rq_hdQFMgWyrq.yaml b/docs/yaak/yaak.rq_hdQFMgWyrq.yaml new file mode 100644 index 0000000..12c0cbc --- /dev/null +++ b/docs/yaak/yaak.rq_hdQFMgWyrq.yaml @@ -0,0 +1,28 @@ +type: http_request +model: http_request +id: rq_hdQFMgWyrq +createdAt: 2025-12-22T17:06:16.224935675 +updatedAt: 2025-12-22T22:01:44.917855863 +workspaceId: wk_yK68KSnsqe +folderId: fl_kD6z8aiTcL +authentication: {} +authenticationType: null +body: + text: |- + { + "username": "Martin Petr", + "email": "me2@martinpetr.dev", + "password": "Aa12345678!" + } +bodyType: application/json +description: '' +headers: +- enabled: true + name: Content-Type + value: application/json + id: GmRxir0VsD +method: POST +name: /users +sortPriority: 0.0 +url: ${[ BASE_URL ]}/users +urlParameters: [] diff --git a/docs/yaak/yaak.wk_yK68KSnsqe.yaml b/docs/yaak/yaak.wk_yK68KSnsqe.yaml new file mode 100644 index 0000000..20cbaa2 --- /dev/null +++ b/docs/yaak/yaak.wk_yK68KSnsqe.yaml @@ -0,0 +1,14 @@ +type: workspace +model: workspace +id: wk_yK68KSnsqe +createdAt: 2025-12-22T17:05:40.658814603 +updatedAt: 2025-12-22T17:05:40.658816978 +authentication: {} +authenticationType: null +description: '' +headers: [] +name: Cashlow +encryptionKeyChallenge: null +settingValidateCertificates: true +settingFollowRedirects: true +settingRequestTimeout: 0 diff --git a/packages/backend/bun.lock b/packages/backend/bun.lock index 7b1e18b..df05d22 100644 --- a/packages/backend/bun.lock +++ b/packages/backend/bun.lock @@ -8,6 +8,9 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "axios": "^1.13.2", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -42,6 +45,7 @@ }, }, "trustedDependencies": [ + "bcrypt", "unrs-resolver", "@nestjs/core", "@swc/core", @@ -423,6 +427,8 @@ "@types/supertest": ["@types/supertest@6.0.3", "", { "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, "sha512-8WzXq62EXFhJ7QsH3Ocb/iKQ/Ty9ZVWnVzoTKc9tyyFRRF3a74Tk2+TLFgaFFw364Ere+npzHKEJ6ga2LzIL7w=="], + "@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="], + "@types/yargs": ["@types/yargs@17.0.35", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg=="], "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], @@ -609,6 +615,8 @@ "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + "bin-version": ["bin-version@6.0.0", "", { "dependencies": { "execa": "^5.0.0", "find-versions": "^5.0.0" } }, "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw=="], "bin-version-check": ["bin-version-check@5.1.0", "", { "dependencies": { "bin-version": "^6.0.0", "semver": "^7.5.3", "semver-truncate": "^3.0.0" } }, "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g=="], @@ -667,6 +675,10 @@ "cjs-module-lexer": ["cjs-module-lexer@2.1.1", "", {}, "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ=="], + "class-transformer": ["class-transformer@0.5.1", "", {}, "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw=="], + + "class-validator": ["class-validator@0.14.3", "", { "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", "validator": "^13.15.20" } }, "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA=="], + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -1085,6 +1097,8 @@ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "libphonenumber-js": ["libphonenumber-js@1.12.33", "", {}, "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw=="], + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "load-esm": ["load-esm@1.0.3", "", {}, "sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA=="], @@ -1161,8 +1175,12 @@ "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + "node-emoji": ["node-emoji@1.11.0", "", { "dependencies": { "lodash": "^4.17.21" } }, "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A=="], + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -1489,6 +1507,8 @@ "v8-to-istanbul": ["v8-to-istanbul@9.3.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^2.0.0" } }, "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA=="], + "validator": ["validator@13.15.26", "", {}, "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], diff --git a/packages/backend/dist/app.controller.js b/packages/backend/dist/app.controller.js deleted file mode 100644 index 6e3d4cc..0000000 --- a/packages/backend/dist/app.controller.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { - value: true -}); -Object.defineProperty(exports, "AppController", { - enumerable: true, - get: function() { - return AppController; - } -}); -const _common = require("@nestjs/common"); -const _appservice = require("./app.service"); -function _ts_decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} -function _ts_metadata(k, v) { - if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); -} -let AppController = class AppController { - getHello() { - return this.appService.getHello(); - } - constructor(appService){ - this.appService = appService; - } -}; -_ts_decorate([ - (0, _common.Get)(), - _ts_metadata("design:type", Function), - _ts_metadata("design:paramtypes", []), - _ts_metadata("design:returntype", String) -], AppController.prototype, "getHello", null); -AppController = _ts_decorate([ - (0, _common.Controller)(), - _ts_metadata("design:type", Function), - _ts_metadata("design:paramtypes", [ - typeof _appservice.AppService === "undefined" ? Object : _appservice.AppService - ]) -], AppController); - -//# sourceMappingURL=app.controller.js.map \ No newline at end of file diff --git a/packages/backend/dist/app.controller.js.map b/packages/backend/dist/app.controller.js.map deleted file mode 100644 index b5a8eaa..0000000 --- a/packages/backend/dist/app.controller.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/app.controller.ts"],"sourcesContent":["import { Controller, Get } from '@nestjs/common';\nimport { AppService } from './app.service';\n\n@Controller()\nexport class AppController {\n constructor(private readonly appService: AppService) {}\n\n @Get()\n getHello(): string {\n return this.appService.getHello();\n }\n}\n"],"names":["AppController","getHello","appService"],"mappings":";;;;+BAIaA;;;eAAAA;;;wBAJmB;4BACL;;;;;;;;;;AAGpB,IAAA,AAAMA,gBAAN,MAAMA;IAIXC,WAAmB;QACjB,OAAO,IAAI,CAACC,UAAU,CAACD,QAAQ;IACjC;IALA,YAAY,AAAiBC,UAAsB,CAAE;aAAxBA,aAAAA;IAAyB;AAMxD"} \ No newline at end of file diff --git a/packages/backend/dist/app.controller.spec.js b/packages/backend/dist/app.controller.spec.js deleted file mode 100644 index 18c9a04..0000000 --- a/packages/backend/dist/app.controller.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { - value: true -}); -const _testing = require("@nestjs/testing"); -const _appcontroller = require("./app.controller"); -const _appservice = require("./app.service"); -describe('AppController', ()=>{ - let appController; - beforeEach(async ()=>{ - const app = await _testing.Test.createTestingModule({ - controllers: [ - _appcontroller.AppController - ], - providers: [ - _appservice.AppService - ] - }).compile(); - appController = app.get(_appcontroller.AppController); - }); - describe('root', ()=>{ - it('should return "Hello World!"', ()=>{ - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); - -//# sourceMappingURL=app.controller.spec.js.map \ No newline at end of file diff --git a/packages/backend/dist/app.controller.spec.js.map b/packages/backend/dist/app.controller.spec.js.map deleted file mode 100644 index d1fd2d4..0000000 --- a/packages/backend/dist/app.controller.spec.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/app.controller.spec.ts"],"sourcesContent":["import { Test, TestingModule } from '@nestjs/testing';\nimport { AppController } from './app.controller';\nimport { AppService } from './app.service';\n\ndescribe('AppController', () => {\n let appController: AppController;\n\n beforeEach(async () => {\n const app: TestingModule = await Test.createTestingModule({\n controllers: [AppController],\n providers: [AppService],\n }).compile();\n\n appController = app.get(AppController);\n });\n\n describe('root', () => {\n it('should return \"Hello World!\"', () => {\n expect(appController.getHello()).toBe('Hello World!');\n });\n });\n});\n"],"names":["describe","appController","beforeEach","app","Test","createTestingModule","controllers","AppController","providers","AppService","compile","get","it","expect","getHello","toBe"],"mappings":";;;;yBAAoC;+BACN;4BACH;AAE3BA,SAAS,iBAAiB;IACxB,IAAIC;IAEJC,WAAW;QACT,MAAMC,MAAqB,MAAMC,aAAI,CAACC,mBAAmB,CAAC;YACxDC,aAAa;gBAACC,4BAAa;aAAC;YAC5BC,WAAW;gBAACC,sBAAU;aAAC;QACzB,GAAGC,OAAO;QAEVT,gBAAgBE,IAAIQ,GAAG,CAAgBJ,4BAAa;IACtD;IAEAP,SAAS,QAAQ;QACfY,GAAG,gCAAgC;YACjCC,OAAOZ,cAAca,QAAQ,IAAIC,IAAI,CAAC;QACxC;IACF;AACF"} \ No newline at end of file diff --git a/packages/backend/dist/app.module.js b/packages/backend/dist/app.module.js index 26fe388..dc3a28f 100644 --- a/packages/backend/dist/app.module.js +++ b/packages/backend/dist/app.module.js @@ -9,8 +9,10 @@ Object.defineProperty(exports, "AppModule", { } }); const _common = require("@nestjs/common"); -const _appcontroller = require("./app.controller"); -const _appservice = require("./app.service"); +const _appcontroller = require("./routes/app.controller"); +const _appservice = require("./routes/app.service"); +const _financemodule = require("./routes/finance/finance.module"); +const _usersmodule = require("./routes/users/users.module"); function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); @@ -21,7 +23,10 @@ let AppModule = class AppModule { }; AppModule = _ts_decorate([ (0, _common.Module)({ - imports: [], + imports: [ + _usersmodule.UsersModule, + _financemodule.FinanceModule + ], controllers: [ _appcontroller.AppController ], diff --git a/packages/backend/dist/app.module.js.map b/packages/backend/dist/app.module.js.map index 7ab1cc0..19913bf 100644 --- a/packages/backend/dist/app.module.js.map +++ b/packages/backend/dist/app.module.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/app.module.ts"],"sourcesContent":["import { Module } from '@nestjs/common';\nimport { AppController } from './app.controller';\nimport { AppService } from './app.service';\n\n@Module({\n imports: [],\n controllers: [AppController],\n providers: [AppService],\n})\nexport class AppModule {}\n"],"names":["AppModule","imports","controllers","AppController","providers","AppService"],"mappings":";;;;+BASaA;;;eAAAA;;;wBATU;+BACO;4BACH;;;;;;;AAOpB,IAAA,AAAMA,YAAN,MAAMA;AAAW;;;QAJtBC,SAAS,EAAE;QACXC,aAAa;YAACC,4BAAa;SAAC;QAC5BC,WAAW;YAACC,sBAAU;SAAC"} \ No newline at end of file +{"version":3,"sources":["../src/app.module.ts"],"sourcesContent":["import { Module } from '@nestjs/common';\nimport { AppController } from './routes/app.controller';\nimport { AppService } from './routes/app.service';\nimport { FinanceModule } from './routes/finance/finance.module';\nimport { UsersModule } from './routes/users/users.module';\n\n@Module({\n imports: [UsersModule, FinanceModule],\n controllers: [AppController],\n providers: [AppService],\n})\nexport class AppModule {}\n"],"names":["AppModule","imports","UsersModule","FinanceModule","controllers","AppController","providers","AppService"],"mappings":";;;;+BAWaA;;;eAAAA;;;wBAXU;+BACO;4BACH;+BACG;6BACF;;;;;;;AAOrB,IAAA,AAAMA,YAAN,MAAMA;AAAW;;;QAJtBC,SAAS;YAACC,wBAAW;YAAEC,4BAAa;SAAC;QACrCC,aAAa;YAACC,4BAAa;SAAC;QAC5BC,WAAW;YAACC,sBAAU;SAAC"} \ No newline at end of file diff --git a/packages/backend/dist/app.service.js b/packages/backend/dist/app.service.js deleted file mode 100644 index 4541f49..0000000 --- a/packages/backend/dist/app.service.js +++ /dev/null @@ -1,27 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { - value: true -}); -Object.defineProperty(exports, "AppService", { - enumerable: true, - get: function() { - return AppService; - } -}); -const _common = require("@nestjs/common"); -function _ts_decorate(decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -} -let AppService = class AppService { - getHello() { - return 'Hello World!'; - } -}; -AppService = _ts_decorate([ - (0, _common.Injectable)() -], AppService); - -//# sourceMappingURL=app.service.js.map \ No newline at end of file diff --git a/packages/backend/dist/app.service.js.map b/packages/backend/dist/app.service.js.map deleted file mode 100644 index bf86c92..0000000 --- a/packages/backend/dist/app.service.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["../src/app.service.ts"],"sourcesContent":["import { Injectable } from '@nestjs/common';\n\n@Injectable()\nexport class AppService {\n getHello(): string {\n return 'Hello World!';\n }\n}\n"],"names":["AppService","getHello"],"mappings":";;;;+BAGaA;;;eAAAA;;;wBAHc;;;;;;;AAGpB,IAAA,AAAMA,aAAN,MAAMA;IACXC,WAAmB;QACjB,OAAO;IACT;AACF"} \ No newline at end of file diff --git a/packages/backend/dist/main.js b/packages/backend/dist/main.js index 8c74db9..32712ed 100644 --- a/packages/backend/dist/main.js +++ b/packages/backend/dist/main.js @@ -4,8 +4,12 @@ Object.defineProperty(exports, "__esModule", { }); const _core = require("@nestjs/core"); const _appmodule = require("./app.module"); +const _Database = require("./classes/database/Database"); +const _common = require("@nestjs/common"); async function bootstrap() { const app = await _core.NestFactory.create(_appmodule.AppModule); + app.useGlobalPipes(new _common.ValidationPipe()); + await _Database.Database.initialize(); await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/packages/backend/dist/main.js.map b/packages/backend/dist/main.js.map index 85a826b..ebd377c 100644 --- a/packages/backend/dist/main.js.map +++ b/packages/backend/dist/main.js.map @@ -1 +1 @@ -{"version":3,"sources":["../src/main.ts"],"sourcesContent":["import { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n await app.listen(process.env.PORT ?? 3000);\n}\nbootstrap();\n"],"names":["bootstrap","app","NestFactory","create","AppModule","listen","process","env","PORT"],"mappings":";;;;sBAA4B;2BACF;AAE1B,eAAeA;IACb,MAAMC,MAAM,MAAMC,iBAAW,CAACC,MAAM,CAACC,oBAAS;IAC9C,MAAMH,IAAII,MAAM,CAACC,QAAQC,GAAG,CAACC,IAAI,IAAI;AACvC;AACAR"} \ No newline at end of file +{"version":3,"sources":["../src/main.ts"],"sourcesContent":["import { NestFactory } from '@nestjs/core';\nimport { AppModule } from './app.module';\nimport { Database } from './classes/database/Database';\nimport { ValidationPipe } from '@nestjs/common';\n\nasync function bootstrap() {\n const app = await NestFactory.create(AppModule);\n\n app.useGlobalPipes(new ValidationPipe());\n\n await Database.initialize();\n\n await app.listen(process.env.PORT ?? 3000);\n}\nbootstrap();\n"],"names":["bootstrap","app","NestFactory","create","AppModule","useGlobalPipes","ValidationPipe","Database","initialize","listen","process","env","PORT"],"mappings":";;;;sBAA4B;2BACF;0BACD;wBACM;AAE/B,eAAeA;IACb,MAAMC,MAAM,MAAMC,iBAAW,CAACC,MAAM,CAACC,oBAAS;IAE9CH,IAAII,cAAc,CAAC,IAAIC,sBAAc;IAErC,MAAMC,kBAAQ,CAACC,UAAU;IAEzB,MAAMP,IAAIQ,MAAM,CAACC,QAAQC,GAAG,CAACC,IAAI,IAAI;AACvC;AACAZ"} \ No newline at end of file diff --git a/packages/backend/package.json b/packages/backend/package.json index 6bd53d8..2804d6e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -24,6 +24,9 @@ "@nestjs/core": "^11.0.1", "@nestjs/platform-express": "^11.0.1", "axios": "^1.13.2", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.3", "pg": "^8.16.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", @@ -75,6 +78,7 @@ "trustedDependencies": [ "@nestjs/core", "@swc/core", + "bcrypt", "unrs-resolver" ] } diff --git a/packages/backend/src/app.module.ts b/packages/backend/src/app.module.ts index 8662803..fa08473 100644 --- a/packages/backend/src/app.module.ts +++ b/packages/backend/src/app.module.ts @@ -1,9 +1,11 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; +import { AppController } from './routes/app.controller'; +import { AppService } from './routes/app.service'; +import { FinanceModule } from './routes/finance/finance.module'; +import { UsersModule } from './routes/users/users.module'; @Module({ - imports: [], + imports: [UsersModule, FinanceModule], controllers: [AppController], providers: [AppService], }) diff --git a/packages/backend/src/app.service.ts b/packages/backend/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/packages/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/packages/backend/src/classes/auth/AuthManager.ts b/packages/backend/src/classes/auth/AuthManager.ts new file mode 100644 index 0000000..c701ba4 --- /dev/null +++ b/packages/backend/src/classes/auth/AuthManager.ts @@ -0,0 +1,43 @@ +import { BadRequestException } from '@nestjs/common'; +import { Database } from '../database/Database'; +import { UserDbEntity } from '../database/entities/User.entity'; +import { PasswordHasher } from '../cryptography/PasswordHasher'; +import { HashGenerator } from '../cryptography/HashGenerator'; +import { randomUUID } from 'crypto'; + +export class AuthManager { + constructor() {} + + static async createUserAccount( + email: string, + password: string, + username: string, + ) { + const existingUser = await this.#getUserByEmail(email); + if (existingUser) return new BadRequestException('User already exists'); + + const passwordHash = await PasswordHasher.hash(password); + const userHash = HashGenerator.generate(64); + + const user = new UserDbEntity(); + user.id = randomUUID(); + user.email = email; + user.password = passwordHash; + user.username = username; + user.hash = userHash; + + await Database.repositories.users.save(user); + + return { + id: user.id, + email: user.email, + username: user.username, + }; + } + + static async #getUserByEmail(email: string): Promise { + if (!email || typeof email != 'string') return null; + + return await Database.repositories.users.findOneBy({ email }); + } +} diff --git a/packages/backend/src/classes/cryptography/HashGenerator.ts b/packages/backend/src/classes/cryptography/HashGenerator.ts new file mode 100644 index 0000000..5950c93 --- /dev/null +++ b/packages/backend/src/classes/cryptography/HashGenerator.ts @@ -0,0 +1,7 @@ +import { randomBytes } from 'crypto'; + +export class HashGenerator { + static generate(length: number) { + return randomBytes(length / 2).toString('hex'); + } +} diff --git a/packages/backend/src/classes/cryptography/PasswordHasher.ts b/packages/backend/src/classes/cryptography/PasswordHasher.ts new file mode 100644 index 0000000..e6a62ea --- /dev/null +++ b/packages/backend/src/classes/cryptography/PasswordHasher.ts @@ -0,0 +1,16 @@ +import * as bc from 'bcrypt'; +import { AUTH_PASSWORD_HASHING_ROUNDS } from 'src/config'; + +export class PasswordHasher { + static async hash(password: string): Promise { + return await new Promise((r) => + bc.hash(password, AUTH_PASSWORD_HASHING_ROUNDS, (e, hash) => r(hash)), + ); + } + + static async compare(password: string, hash: string): Promise { + return await new Promise((r) => + bc.compare(password, hash, (e, res) => r(res)), + ); + } +} diff --git a/packages/backend/src/classes/database/Database.ts b/packages/backend/src/classes/database/Database.ts new file mode 100644 index 0000000..9846e17 --- /dev/null +++ b/packages/backend/src/classes/database/Database.ts @@ -0,0 +1,60 @@ +import { DB_DATABASE, DB_HOST, DB_PASSWORD, DB_USERNAME } from 'src/config'; +import { DataSource, Repository } from 'typeorm'; +import { UserDbEntity } from './entities/User.entity'; +import { BankAccountDbEntity } from './entities/BankAccount.entity'; + +const ENTITIES = { + users: UserDbEntity, + bankAccounts: BankAccountDbEntity, +}; + +export class Database { + static instance: Database = new Database(); + + #dataSource: DataSource; + + #dbType: 'postgres' = 'postgres'; + #dbHost = DB_HOST; + #dbPort = 5432; + #dbUsername = DB_USERNAME; + #dbPassword = DB_PASSWORD; + #dbDatabase = DB_DATABASE; + + #entities = Object.values(ENTITIES); + + constructor() { + this.#dataSource = new DataSource({ + type: this.#dbType, + host: this.#dbHost, + port: this.#dbPort, + username: this.#dbUsername, + password: this.#dbPassword, + database: this.#dbDatabase, + synchronize: true, + entities: this.#entities, + }); + } + + get repositories() { + return { + users: this.#dataSource.getRepository(UserDbEntity), + bankAccounts: this.#dataSource.getRepository(BankAccountDbEntity), + } as { + [K in keyof typeof ENTITIES]: Repository< + InstanceType<(typeof ENTITIES)[K]> + >; + }; + } + + static get repositories() { + return this.instance.repositories; + } + + async initialize() { + await this.#dataSource.initialize(); + } + + static async initialize() { + await this.instance.initialize(); + } +} diff --git a/packages/backend/src/classes/database/entities/BankAccount.entity.ts b/packages/backend/src/classes/database/entities/BankAccount.entity.ts new file mode 100644 index 0000000..77817b3 --- /dev/null +++ b/packages/backend/src/classes/database/entities/BankAccount.entity.ts @@ -0,0 +1,26 @@ +import { + Column, + Entity, + ManyToOne, + PrimaryColumn, + type Relation, +} from 'typeorm'; +import { UserDbEntity } from './User.entity'; + +@Entity('bank_account') +export class BankAccountDbEntity { + @PrimaryColumn({ + type: 'varchar', + length: 35, + }) + id: string; + + @Column({ + type: 'varchar', + length: 100, + }) + name: string; + + @ManyToOne(() => UserDbEntity, (user) => user.bankAccounts) + user: Relation; +} diff --git a/packages/backend/src/classes/database/entities/User.entity.ts b/packages/backend/src/classes/database/entities/User.entity.ts new file mode 100644 index 0000000..e84189c --- /dev/null +++ b/packages/backend/src/classes/database/entities/User.entity.ts @@ -0,0 +1,44 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToMany, + OneToMany, + PrimaryColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; +import { BankAccountDbEntity } from './BankAccount.entity'; + +@Entity('user') +export class UserDbEntity { + @PrimaryColumn({ + type: 'varchar', + length: 36, + }) + id: string; + + @Column() + email: string; + + @Column() + password: string; + + @Column() + username: string; + + @Column({ + type: 'varchar', + length: 64, + }) + hash: string; + + @CreateDateColumn() + createdAt: Date; + + @UpdateDateColumn() + updatedAt: Date; + + @OneToMany(() => BankAccountDbEntity, (bankAccount) => bankAccount.user) + bankAccounts: Relation[]; +} diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts new file mode 100644 index 0000000..19d773d --- /dev/null +++ b/packages/backend/src/config.ts @@ -0,0 +1,6 @@ +export const DB_HOST = process.env.IP_DATABASE; +export const DB_USERNAME = process.env.POSTGRES_USER; +export const DB_PASSWORD = process.env.POSTGRES_PASSWORD; +export const DB_DATABASE = process.env.POSTGRES_DB; + +export const AUTH_PASSWORD_HASHING_ROUNDS = 14; diff --git a/packages/backend/src/dto/CreateUserAccount.dto.ts b/packages/backend/src/dto/CreateUserAccount.dto.ts new file mode 100644 index 0000000..c595e31 --- /dev/null +++ b/packages/backend/src/dto/CreateUserAccount.dto.ts @@ -0,0 +1,23 @@ +import { + IsEmail, + IsString, + IsStrongPassword, + Length, + MaxLength, + MinLength, +} from 'class-validator'; + +export class CreateUserAccountDto { + @IsString() + @IsEmail() + email: string; + + @IsString() + @IsStrongPassword() + @MaxLength(64) + password: string; + + @IsString() + @Length(3, 64) + username: string; +} diff --git a/packages/backend/src/main.ts b/packages/backend/src/main.ts index f76bc8d..a880c8f 100644 --- a/packages/backend/src/main.ts +++ b/packages/backend/src/main.ts @@ -1,8 +1,15 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; +import { Database } from './classes/database/Database'; +import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + app.useGlobalPipes(new ValidationPipe()); + + await Database.initialize(); + await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/packages/backend/src/app.controller.spec.ts b/packages/backend/src/routes/app.controller.spec.ts similarity index 74% rename from packages/backend/src/app.controller.spec.ts rename to packages/backend/src/routes/app.controller.spec.ts index d22f389..d6a9ec6 100644 --- a/packages/backend/src/app.controller.spec.ts +++ b/packages/backend/src/routes/app.controller.spec.ts @@ -14,9 +14,9 @@ describe('AppController', () => { appController = app.get(AppController); }); - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); + // describe('root', () => { + // it('should return "Hello World!"', () => { + // expect(appController.getHello()).toBe('Hello World!'); + // }); + // }); }); diff --git a/packages/backend/src/app.controller.ts b/packages/backend/src/routes/app.controller.ts similarity index 72% rename from packages/backend/src/app.controller.ts rename to packages/backend/src/routes/app.controller.ts index cce879e..4454e3e 100644 --- a/packages/backend/src/app.controller.ts +++ b/packages/backend/src/routes/app.controller.ts @@ -4,9 +4,4 @@ import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } } diff --git a/packages/backend/src/routes/app.service.ts b/packages/backend/src/routes/app.service.ts new file mode 100644 index 0000000..7263d33 --- /dev/null +++ b/packages/backend/src/routes/app.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AppService {} diff --git a/packages/backend/src/routes/finance/account/account.controller.ts b/packages/backend/src/routes/finance/account/account.controller.ts new file mode 100644 index 0000000..49dcde7 --- /dev/null +++ b/packages/backend/src/routes/finance/account/account.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { AccountService } from './account.service'; + +@Controller('/finance/account') +export class AccountController { + constructor(private readonly accountService: AccountService) {} +} diff --git a/packages/backend/src/routes/finance/account/account.module.ts b/packages/backend/src/routes/finance/account/account.module.ts new file mode 100644 index 0000000..32a4259 --- /dev/null +++ b/packages/backend/src/routes/finance/account/account.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { AccountController } from './account.controller'; +import { AccountService } from './account.service'; + +@Module({ + imports: [], + controllers: [AccountController], + providers: [AccountService], +}) +export class AccountModule {} diff --git a/packages/backend/src/routes/finance/account/account.service.ts b/packages/backend/src/routes/finance/account/account.service.ts new file mode 100644 index 0000000..682f9a4 --- /dev/null +++ b/packages/backend/src/routes/finance/account/account.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class AccountService {} diff --git a/packages/backend/src/routes/finance/finance.module.ts b/packages/backend/src/routes/finance/finance.module.ts new file mode 100644 index 0000000..19c212b --- /dev/null +++ b/packages/backend/src/routes/finance/finance.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { AccountModule } from './account/account.module'; + +@Module({ + imports: [AccountModule], + controllers: [], + providers: [], +}) +export class FinanceModule {} diff --git a/packages/backend/src/routes/users/users.controller.ts b/packages/backend/src/routes/users/users.controller.ts new file mode 100644 index 0000000..ff59bcd --- /dev/null +++ b/packages/backend/src/routes/users/users.controller.ts @@ -0,0 +1,15 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { UsersService } from './users.service'; +import { CreateUserAccountDto } from 'src/dto/CreateUserAccount.dto'; + +@Controller('/users') +export class UsersController { + constructor(private readonly usersService: UsersService) {} + + @Post('/') + async createUserAccount( + @Body() { email, password, username }: CreateUserAccountDto, + ) { + return await this.usersService.createUserAccount(email, password, username); + } +} diff --git a/packages/backend/src/routes/users/users.module.ts b/packages/backend/src/routes/users/users.module.ts new file mode 100644 index 0000000..4fc8daf --- /dev/null +++ b/packages/backend/src/routes/users/users.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; + +@Module({ + imports: [], + controllers: [UsersController], + providers: [UsersService], +}) +export class UsersModule {} diff --git a/packages/backend/src/routes/users/users.service.ts b/packages/backend/src/routes/users/users.service.ts new file mode 100644 index 0000000..3461763 --- /dev/null +++ b/packages/backend/src/routes/users/users.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { AuthManager } from 'src/classes/auth/AuthManager'; + +@Injectable() +export class UsersService { + async createUserAccount(email: string, password: string, username: string) { + return await AuthManager.createUserAccount(email, password, username); + } +}