diff --git a/package.json b/package.json index 4c88499..8c4624f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@nestjs/platform-express": "^10.2.10", "@prisma/client": "^5.7.0", "@techmmunity/utils": "^1.10.1", + "axios": "^1.6.5", "change-case": "^5.2.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -76,7 +77,8 @@ "prepare": "husky install && yarn db:prisma", "db:prisma": "yarn lint:prisma && prisma generate --generator client", "db:docs": "yarn lint:prisma && prisma generate --generator dbml && dbdocs build ./prisma/schema.dbml", - "db:migrations": "yarn lint:prisma && prisma migrate dev --name", + "db:gen-migration": "yarn lint:prisma && prisma migrate dev --name", + "db:migrate": "prisma migrate deploy", "openapi:serve": "redocly preview-docs", "openapi:postman": "yarn lint:openapi && redocly bundle -o openapi/bundle.yaml && openapi2postmanv2 -s openapi/bundle.yaml -o openapi/postman.json -O folderStrategy=Tags,requestParametersResolution=Example", "build": "./scripts/build.sh", @@ -84,6 +86,7 @@ "start": "nest start", "start:docker": "nest start --watch", "start:dev": "docker compose up", + "start:db": "docker compose up postgres", "start:prod": "node main", "clean:docker": "docker container rm econominhas-api && docker image rm econominhas-api", "lint:ts": "tsc --project tsconfig.lint.json", diff --git a/prisma/migrations/20240117032215_account_config_relationship/migration.sql b/prisma/migrations/20240117032215_account_config_relationship/migration.sql new file mode 100644 index 0000000..6090ae7 --- /dev/null +++ b/prisma/migrations/20240117032215_account_config_relationship/migration.sql @@ -0,0 +1,99 @@ +/* + Warnings: + + - You are about to drop the column `rt_bill_id` on the `cards` table. All the data in the column will be lost. + - The primary key for the `configs` table will be changed. If it partially fails, the table could be left without primary key constraint. + - You are about to drop the column `created_at` on the `installments` table. All the data in the column will be lost. + - You are about to drop the column `payment_method` on the `recurrent_transactions` table. All the data in the column will be lost. + - You are about to drop the column `payment_method` on the `transactions` table. All the data in the column will be lost. + - A unique constraint covering the columns `[account_id]` on the table `configs` will be added. If there are existing duplicate values, this will fail. + - Added the required column `id` to the `configs` table without a default value. This is not possible if the table is not empty. + - Added the required column `installment_group_id` to the `installments` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateEnum +CREATE TYPE "pay_at_enum" AS ENUM ('STATEMENT', 'DUE'); + +-- DropForeignKey +ALTER TABLE "accounts" DROP CONSTRAINT "Config_id_fkey"; + +-- DropForeignKey +ALTER TABLE "accounts" DROP CONSTRAINT "MagicLinkCode_id_fkey"; + +-- DropForeignKey +ALTER TABLE "cards" DROP CONSTRAINT "cards_rt_bill_id_fkey"; + +-- DropForeignKey +ALTER TABLE "transactions" DROP CONSTRAINT "transactions_id_fkey"; + +-- DropIndex +DROP INDEX "cards_rt_bill_id_key"; + +-- AlterTable +ALTER TABLE "cards" DROP COLUMN "rt_bill_id", +ADD COLUMN "pay_at" "pay_at_enum", +ADD COLUMN "pay_with_id" TEXT; + +-- AlterTable +ALTER TABLE "configs" DROP CONSTRAINT "configs_pkey", +ADD COLUMN "id" CHAR(16) NOT NULL, +ADD CONSTRAINT "configs_pkey" PRIMARY KEY ("id"); + +-- AlterTable +ALTER TABLE "installments" DROP COLUMN "created_at", +ADD COLUMN "card_bill_id" CHAR(16), +ADD COLUMN "installment_group_id" CHAR(16) NOT NULL; + +-- AlterTable +ALTER TABLE "recurrent_transactions" DROP COLUMN "payment_method"; + +-- AlterTable +ALTER TABLE "transactions" DROP COLUMN "payment_method"; + +-- DropEnum +DROP TYPE "payment_method_enum"; + +-- CreateTable +CREATE TABLE "card_bills" ( + "id" CHAR(16) NOT NULL, + "card_id" CHAR(16) NOT NULL, + "month" DATE NOT NULL, + "start_at" DATE NOT NULL, + "end_at" DATE NOT NULL, + "statement_date" DATE NOT NULL, + "due_date" DATE NOT NULL, + "paid_at" TIMESTAMP, + "payment_transaction_id" TEXT, + + CONSTRAINT "card_bills_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "card_bills_payment_transaction_id_key" ON "card_bills"("payment_transaction_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "card_bills_card_id_month_key" ON "card_bills"("card_id", "month"); + +-- CreateIndex +CREATE UNIQUE INDEX "configs_account_id_key" ON "configs"("account_id"); + +-- AddForeignKey +ALTER TABLE "configs" ADD CONSTRAINT "configs_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "magic_link_codes" ADD CONSTRAINT "MagicLinkCode_id_fkey" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "cards" ADD CONSTRAINT "cards_pay_with_id_fkey" FOREIGN KEY ("pay_with_id") REFERENCES "bank_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "card_bills" ADD CONSTRAINT "card_bills_card_id_fkey" FOREIGN KEY ("card_id") REFERENCES "cards"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "card_bills" ADD CONSTRAINT "card_bills_payment_transaction_id_fkey" FOREIGN KEY ("payment_transaction_id") REFERENCES "transactions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "installments" ADD CONSTRAINT "installments_transaction_id_fkey" FOREIGN KEY ("transaction_id") REFERENCES "transactions"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "installments" ADD CONSTRAINT "installments_card_bill_id_fkey" FOREIGN KEY ("card_bill_id") REFERENCES "card_bills"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20240117033028_sign_in_provider_length/migration.sql b/prisma/migrations/20240117033028_sign_in_provider_length/migration.sql new file mode 100644 index 0000000..b176f36 --- /dev/null +++ b/prisma/migrations/20240117033028_sign_in_provider_length/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "sign_in_providers" ALTER COLUMN "access_token" SET DATA TYPE VARCHAR(250), +ALTER COLUMN "refresh_token" SET DATA TYPE VARCHAR(250); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f574ca4..278e1e0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -38,8 +38,8 @@ model Account { phone String? @unique @db.VarChar(25) createdAt DateTime @default(now()) @map("created_at") - config Config @relation(fields: [id], references: [accountId], map: "Config_id_fkey", onDelete: Restrict) - magicLinkCode MagicLinkCode? @relation(fields: [id], references: [accountId], map: "MagicLinkCode_id_fkey", onDelete: Restrict) + config Config? + magicLinkCode MagicLinkCode? signInProviders SignInProvider[] refreshTokens RefreshToken[] userSubscriptions UserSubscription[] @@ -59,8 +59,8 @@ model SignInProvider { accountId String @map("account_id") @db.Char(16) provider SignInProviderEnum providerId String @map("provider_id") @db.VarChar(50) - accessToken String @map("access_token") @db.VarChar(150) - refreshToken String @map("refresh_token") @db.VarChar(150) + accessToken String @map("access_token") @db.VarChar(250) + refreshToken String @map("refresh_token") @db.VarChar(250) expiresAt DateTime @map("expires_at") account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) @@ -71,12 +71,13 @@ model SignInProvider { /// Contains user's account config model Config { - accountId String @id @map("account_id") @db.Char(16) + id String @id @db.Char(16) /// Same as accountId + accountId String @unique @map("account_id") @db.Char(16) name String? @db.VarChar(20) currentBudgetId String? @unique @map("current_budget_id") @db.Char(16) salaryId String? @unique @map("salary_id") @db.Char(16) - account Account? + account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) currentBudget Budget? @relation(fields: [currentBudgetId], references: [id]) salary RecurrentTransaction? @relation(fields: [salaryId], references: [id]) @@ -90,7 +91,7 @@ model MagicLinkCode { isFirstAccess Boolean @map("is_first_access") createdAt DateTime @default(now()) @map("created_at") - account Account? + account Account? @relation(fields: [accountId], references: [id], map: "MagicLinkCode_id_fkey", onDelete: Cascade) @@map("magic_link_codes") } diff --git a/src/adapters/implementations/google/google.service.ts b/src/adapters/implementations/google/google.service.ts index f6e1979..e60be42 100644 --- a/src/adapters/implementations/google/google.service.ts +++ b/src/adapters/implementations/google/google.service.ts @@ -9,6 +9,7 @@ import { DateAdapter } from 'adapters/date'; import { DayjsAdapterService } from '../dayjs/dayjs.service'; import { AppConfig } from 'config'; import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; interface ExchangeCodeAPIOutput { access_token: string; @@ -41,25 +42,28 @@ export class GoogleAdapterService extends GoogleAdapter { code, originUrl, }: ExchangeCodeInput): Promise { + // ALERT: The order of the properties is important, don't change it! const body = new URLSearchParams(); + body.append('code', code); body.append('client_id', this.config.get('GOOGLE_CLIENT_ID')); body.append('client_secret', this.config.get('GOOGLE_CLIENT_SECRET')); - body.append('grant_type', 'authorization_code'); - body.append('code', code); if (originUrl) { body.append('redirect_uri', originUrl); } + body.append('grant_type', 'authorization_code'); + // ALERT: The order of the properties is important, don't change it! - const result = await fetch('https://oauth2.googleapis.com/token', { - method: 'POST', - body, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json', - }, - }) - .then((r) => r.json()) - .then((r) => r as ExchangeCodeAPIOutput); + const result = await axios + .post('https://oauth2.googleapis.com/token', body, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + }) + .then((r) => r.data as ExchangeCodeAPIOutput) + .catch((err) => { + throw err?.response?.data; + }); return { accessToken: result.access_token, diff --git a/src/delivery/auth.controller.ts b/src/delivery/auth.controller.ts index 33f1bea..8d65df5 100644 --- a/src/delivery/auth.controller.ts +++ b/src/delivery/auth.controller.ts @@ -31,7 +31,7 @@ export class AuthController { async createFromGoogleProvider( @Body() body: CreateFromGoogleProviderDto, - @Res() + @Res({ passthrough: true }) res: Response, ) { const { isFirstAccess, ...data } = @@ -71,7 +71,7 @@ export class AuthController { async exchangeCode( @Body() body: ExchangeCodeDto, - @Res() + @Res({ passthrough: true }) res: Response, ) { const { isFirstAccess, ...data } = diff --git a/src/models/transaction.ts b/src/models/transaction.ts index e008432..5783800 100644 --- a/src/models/transaction.ts +++ b/src/models/transaction.ts @@ -56,6 +56,7 @@ export interface CreateTransferInput { budgetDateId: string; description: string; createdAt: Date; + isSystemManaged: boolean; } export abstract class TransactionRepository { diff --git a/src/repositories/postgres/auth/auth-repository.service.ts b/src/repositories/postgres/auth/auth-repository.service.ts index 6fe56ab..a7bd49e 100644 --- a/src/repositories/postgres/auth/auth-repository.service.ts +++ b/src/repositories/postgres/auth/auth-repository.service.ts @@ -39,13 +39,11 @@ export class AuthRepositoryService extends AuthRepository { async create(i: CreateInput): Promise { const accountId = this.idAdapter.genId(); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore const baseAccount: Prisma.AccountCreateArgs['data'] = { id: accountId, config: { create: { - accountId, + id: accountId, }, }, }; @@ -136,7 +134,7 @@ export class AuthRepositoryService extends AuthRepository { where: { OR: [ { - SignInProvider: { + signInProviders: { every: { provider, providerId, diff --git a/src/repositories/postgres/transaction/transaction-repository.service.ts b/src/repositories/postgres/transaction/transaction-repository.service.ts index f430e1f..ab82306 100644 --- a/src/repositories/postgres/transaction/transaction-repository.service.ts +++ b/src/repositories/postgres/transaction/transaction-repository.service.ts @@ -10,6 +10,7 @@ import type { import { TransactionRepository } from 'models/transaction'; import { UIDAdapterService } from 'adapters/implementations/uid/uid.service'; import { IdAdapter } from 'adapters/id'; +import { TransactionTypeEnum } from '@prisma/client'; @Injectable() export class TransactionRepositoryService extends TransactionRepository { @@ -131,20 +132,37 @@ export class TransactionRepositoryService extends TransactionRepository { budgetDateId, description, createdAt, + isSystemManaged, }: CreateTransferInput): Promise { await this.transactionRepository.create({ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore data: { id: this.idAdapter.genId(), - accountId, name, amount, - bankAccountFromId, - bankAccountToId, - budgetDateId, description, createdAt, + isSystemManaged, + type: TransactionTypeEnum.TRANSFER, + account: { + connect: { + id: accountId, + }, + }, + bankAccountFrom: { + connect: { + id: bankAccountFromId, + }, + }, + bankAccountTo: { + connect: { + id: bankAccountToId, + }, + }, + budgetDate: { + connect: { + id: budgetDateId, + }, + }, }, }); } diff --git a/src/usecases/auth/auth.service.ts b/src/usecases/auth/auth.service.ts index 1ee6a57..28d05b3 100644 --- a/src/usecases/auth/auth.service.ts +++ b/src/usecases/auth/auth.service.ts @@ -3,6 +3,7 @@ import { ConflictException, Inject, Injectable, + Logger, NotFoundException, } from '@nestjs/common'; import type { @@ -42,7 +43,10 @@ interface GenTokensInput { @Injectable() export class AuthService extends AuthUseCase { - private readonly requiredGoogleScopes = ['identify', 'email']; + private readonly requiredGoogleScopes = [ + 'https://www.googleapis.com/auth/userinfo.profile', + 'https://www.googleapis.com/auth/userinfo.email', + ]; constructor( @Inject(AuthRepositoryService) @@ -73,7 +77,9 @@ export class AuthService extends AuthUseCase { }: CreateWith3rdPartyProviderInput): Promise { const { scopes, ...providerTokens } = await this.googleAdapter .exchangeCode({ code, originUrl }) - .catch(() => { + .catch((err) => { + Logger.error(err); + throw new BadRequestException('Invalid code'); }); @@ -279,10 +285,13 @@ export class AuthService extends AuthUseCase { } else { promises.push({ refreshToken: '' }); } + if (!isFirstAccess) { - this.termsAndPoliciesService.hasAcceptedLatest({ - accountId: accountId, - }); + promises.push( + this.termsAndPoliciesService.hasAcceptedLatest({ + accountId: accountId, + }), + ); } else { promises.push(false); } diff --git a/src/usecases/terms-and-policies/terms-and-policies.service.ts b/src/usecases/terms-and-policies/terms-and-policies.service.ts index 5824145..8f7fc69 100644 --- a/src/usecases/terms-and-policies/terms-and-policies.service.ts +++ b/src/usecases/terms-and-policies/terms-and-policies.service.ts @@ -54,6 +54,6 @@ export class TermsAndPoliciesService extends TermsAndPoliciesUseCase { this.termsAndPoliciesRepository.getLatest(), ]); - return latestTermsAccepted.semVer === latestTerms.semVer; + return latestTermsAccepted?.semVer === latestTerms?.semVer; } } diff --git a/src/usecases/transaction/transaction.service.ts b/src/usecases/transaction/transaction.service.ts index fd73d32..74fe5bb 100644 --- a/src/usecases/transaction/transaction.service.ts +++ b/src/usecases/transaction/transaction.service.ts @@ -123,6 +123,7 @@ export class TransactionService extends TransactionUseCase { budgetDateId, description, createdAt, + isSystemManaged: false, }); } } diff --git a/yarn.lock b/yarn.lock index e1b3d23..7861066 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3643,6 +3643,15 @@ axios@0.27.2: follow-redirects "^1.14.9" form-data "^4.0.0" +axios@^1.6.5: + version "1.6.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8" + integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg== + dependencies: + follow-redirects "^1.15.4" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-jest@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" @@ -5341,6 +5350,11 @@ follow-redirects@^1.14.9: resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.15.4: + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== + foreach@^2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.6.tgz#87bcc8a1a0e74000ff2bf9802110708cfb02eb6e" @@ -8028,6 +8042,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + pump@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"