Skip to content

Commit

Permalink
Merge pull request #7 from Webster-FR/feature/banks
Browse files Browse the repository at this point in the history
Merge banks to Main
  • Loading branch information
Xen0Xys authored Jan 6, 2024
2 parents 652ee2a + 8238e81 commit 9c8cf31
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 21 deletions.
130 changes: 130 additions & 0 deletions doc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Encryption des données
## SHA-256
- Tokens/sum
## Argon2id
- Mots de passe
- Tokens
## Symétrique avec clé encryption API
- Secret utilisateur
- Banks/name
## Symétrique secret utilisateur
- Secret 2FA
- Usernames
- Emails
- Wordings
- Accounts/amount
- Todos/name



# For future
## Auth
### GET /auth/2fa
Take JWT. Generate the 2fa secret and send the link+qrcode.
### POST /auth/2fa
Take JWT and 2fa code and verify it's well configured by the user.
### DELETE /auth/2fa
Take JWT. Remove the 2fa from the account.
### POST /auth/2fa/recover
Take JWT and one 2fa recovery code. Disable the 2fa if code is correct.
▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
# Naming
- AT: Access token (classic JWT, have user id, token type)
- RT: Refresh token (JWT, have user id, token type)
- CT: Code token (JWT, have user id, token type, and keep logged value)

# Routes

## Version
### GET /version
Return the current backend version.

## Auth
### POST /auth/login?keep
Take username, password. Return AT and RT if keep param is present. If verification code present, return CT.
### POST /auth/logout
Take AT (and RT if existing) and add them to token blacklist.
### POST /auth/logout/all
Take AT, add all existing tokens to blacklist
### POST /auth/register
Take username, email and password. Return CT and send email with confirmation code.
### POST /auth/refresh
Take old AT and RT, add both to token blacklist and re-send new AT and RT.
### POST /auth/register/confirm
Take CT and confirmation code. Return AT.
### POST /auth/register/confirm/resend
Take CT. Re-create and re-send a new confirmation code by email. Return new CT.

## Users
### GET /users/me
Take JWT and return username and email.
### PATCH /users/me/username
Take JWT and new username.
### PATCH /users/me/password
Take JWT and new password.
### DELETE /users/me
Take JWT.

## Transactions
### GET /transactions
Take JWT, return list of user transactions.
### POST /transactions
Take JWT, transaction type, amount and wording.
### PATCH /transactions/:ulid/wording
Take JWT and new wording.
### POST /transactions/rectification
Take JWT, transaction ulid, new amount. Return updated transaction.
### GET /transactions/categories
Take JWT, return default and users categories.
### POST /transactions/categories
Take JWT, name, icon and color. Return created category.
### PUT /transactions/categories/:id
Take JWT, name, icon and color. Return updated category.

## Recurring transactions
### GET /recurring
Take JWT, return user recurring transactions.
### POST /recurring
Take JWT, wording, type and amount. Return created recurring transaction.
### PUT /recurring/:id
Take JWT, wording and amount. Return updated recurring transaction.
### DELETE /recurring/:id
Take JWT.

## Tips
### GET /tips/tod
Return tips of the day.

## Accounts
### GET /accounts
Take JWT, return all user accounts.
### POST /accounts
Take JWT, name, bank id and current amount, return created account.
### PATCH /accounts/:id/name
Take JWT and name, return updated account.
### DELETE /accounts/:id
Take JWT.

## Banks
### GET /banks
Take JWT, return user banks and default banks.
### POST /banks
Take JWT and bank name, return created bank.
### PATCH /banks/:id/name
Take JWT and name, return updated bank.
### DELETE /banks/:id
Take JWT.

## Todos
### GET /todos
Take JWT. Return all user todos
### POST /todos
Take JWT, name, deadline, parent id, icon, color and recurring. Return created todo.
### PATCH /todos/:id/parent
Take JWT and new parent id. Return updated todo.
### PATCH /todos/:id/completed
Take JWT and new completed value. Return updated todo.
### PUT /todos/:id
Take JWT, name, deadline, icon, color, completed value and frequency. Return updated todo.
### DELETE /todos/:id
Take JWT.
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ CREATE UNIQUE INDEX "tips_tips_key" ON "tips"("tips");
CREATE UNIQUE INDEX "tips_order_key" ON "tips"("order");

-- CreateIndex
CREATE UNIQUE INDEX "banks_name_key" ON "banks"("name");
CREATE UNIQUE INDEX "banks_name_user_id_key" ON "banks"("name", "user_id");

-- CreateIndex
CREATE UNIQUE INDEX "accounts_name_key" ON "accounts"("name");
Expand Down
3 changes: 2 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ model Tips {

model Banks {
id Int @id @default(autoincrement())
name String @unique
name String
user_id Int?
user User? @relation(fields: [user_id], references: [id])
account Accounts[]
@@unique([name, user_id])
@@map("banks")
}

Expand Down
7 changes: 4 additions & 3 deletions prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const encryptionService = new EncryptionService();

async function main(){
const userSecret = encryptionService.generateSecret();
const encryptionKey = process.env.SYMMETRIC_ENCRYPTION_KEY;
const testUser = await prisma.user.upsert({
where: {id: 1},
update: {},
Expand All @@ -20,7 +21,7 @@ async function main(){
username: encryptionService.encryptSymmetric("test", userSecret),
email: "test@exemple.org",
password: await encryptionService.hash("password"),
secret: encryptionService.encryptSymmetric(userSecret, process.env.SYMMETRIC_ENCRYPTION_KEY),
secret: encryptionService.encryptSymmetric(userSecret, encryptionKey),
verification_code_id: null,
created_at: new Date(),
updated_at: new Date(),
Expand Down Expand Up @@ -79,7 +80,7 @@ async function main(){
update: {},
create: {
id: 1,
name: "Default bank",
name: encryptionService.encryptSymmetric("Default bank", encryptionKey),
},
});

Expand All @@ -89,7 +90,7 @@ async function main(){
create: {
id: 2,
user_id: 1,
name: "User bank",
name: encryptionService.encryptSymmetric("User bank", encryptionKey),
},
});

Expand Down
3 changes: 2 additions & 1 deletion src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import {VerificationCodesModule} from "./verification-codes/verification-codes.m
import {TodosModule} from "./todos/todos.module";
import {TipsModule} from "./tips/tips.module";
import {MaintenanceModule} from "./maintenance/maintenance.module";
import {BanksModule} from "./banks/banks.module";

@Module({
imports: [AuthModule, VersionModule, ConfigModule.forRoot({isGlobal: true}), UsersModule, VerificationCodesModule, TodosModule, TipsModule, MaintenanceModule],
imports: [AuthModule, VersionModule, ConfigModule.forRoot({isGlobal: true}), UsersModule, VerificationCodesModule, TodosModule, TipsModule, MaintenanceModule, BanksModule],
})
export class AppModule{}
57 changes: 57 additions & 0 deletions src/banks/banks.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {Body, Controller, Delete, Get, HttpStatus, Param, Patch, Post, Req, UseGuards} from "@nestjs/common";
import {MaintenanceGuard} from "../maintenance/guards/maintenance.guard";
import {ApiBearerAuth, ApiResponse} from "@nestjs/swagger";
import {AtGuard} from "../auth/guards/at.guard";
import {BanksService} from "./banks.service";
import {BankEntity} from "./models/entities/bank.entity";
import {BankNameDto} from "./models/dto/bank-name.dto";
import {IdDto} from "../models/dto/id.dto";

@Controller("banks")
@UseGuards(MaintenanceGuard)
export class BanksController{
constructor(
private readonly banksService: BanksService
){}

@Get()
@UseGuards(AtGuard)
@ApiBearerAuth()
@ApiResponse({status: HttpStatus.OK, description: "User and default banks returned", type: BankEntity, isArray: true})
@ApiResponse({status: HttpStatus.UNAUTHORIZED, description: "Invalid or missing access token"})
@ApiResponse({status: HttpStatus.NOT_FOUND, description: "User not found"})
async getBanks(@Req() req: any): Promise<BankEntity[]>{
return this.banksService.getBanks(req.user.id);
}

@Post()
@UseGuards(AtGuard)
@ApiBearerAuth()
@ApiResponse({status: HttpStatus.CREATED, description: "Bank added", type: BankEntity})
@ApiResponse({status: HttpStatus.UNAUTHORIZED, description: "Invalid or missing access token"})
@ApiResponse({status: HttpStatus.NOT_FOUND, description: "User not found"})
@ApiResponse({status: HttpStatus.CONFLICT, description: "Bank already exists"})
async addBank(@Req() req: any, @Body() bankNameDto: BankNameDto): Promise<BankEntity>{
return this.banksService.addBank(req.user.id, bankNameDto.name);
}

@Patch(":id/name")
@UseGuards(AtGuard)
@ApiBearerAuth()
@ApiResponse({status: HttpStatus.OK, description: "Bank name updated", type: BankEntity})
@ApiResponse({status: HttpStatus.UNAUTHORIZED, description: "Invalid or missing access token"})
@ApiResponse({status: HttpStatus.NOT_FOUND, description: "User or bank not found"})
async updateBankName(@Req() req: any, @Param() idDto: IdDto, @Body() bankNameDto: BankNameDto): Promise<BankEntity>{
return this.banksService.updateBankName(req.user.id, idDto.id, bankNameDto.name);
}

@Delete(":id")
@UseGuards(AtGuard)
@ApiBearerAuth()
@ApiResponse({status: HttpStatus.OK, description: "Bank deleted", type: BankEntity})
@ApiResponse({status: HttpStatus.UNAUTHORIZED, description: "Invalid or missing access token"})
@ApiResponse({status: HttpStatus.NOT_FOUND, description: "User or bank not found"})
async deleteBank(@Req() req: any, @Param() idDto: IdDto): Promise<BankEntity>{
return this.banksService.deleteBank(req.user.id, idDto.id);
}
}
12 changes: 12 additions & 0 deletions src/banks/banks.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {Module} from "@nestjs/common";
import {BanksService} from "./banks.service";
import {BanksController} from "./banks.controller";
import {ServicesModule} from "../services/services.module";
import {UsersModule} from "../users/users.module";

@Module({
controllers: [BanksController],
providers: [BanksService],
imports: [ServicesModule, UsersModule],
})
export class BanksModule{}
97 changes: 97 additions & 0 deletions src/banks/banks.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {Injectable, NotFoundException} from "@nestjs/common";
import {BankEntity} from "./models/entities/bank.entity";
import {PrismaService} from "../services/prisma.service";
import {ConfigService} from "@nestjs/config";
import {EncryptionService} from "../services/encryption.service";
import {UsersService} from "../users/users.service";

@Injectable()
export class BanksService{

constructor(
private readonly prismaService: PrismaService,
private readonly configService: ConfigService,
private readonly encryptionService: EncryptionService,
private readonly usersService: UsersService,
){}

async getBanks(userId: number): Promise<BankEntity[]>{
if(!await this.usersService.isUserExists(userId))
throw new NotFoundException("User not found");
const banks: BankEntity[] = await this.prismaService.banks.findMany({
where: {
OR: [
{user_id: userId},
{user_id: null},
],
}
});
for(const bank of banks)
bank.name = this.encryptionService.decryptSymmetric(bank.name, this.configService.get("SYMMETRIC_ENCRYPTION_KEY"));
return banks;
}

async addBank(userId: number, bankName: string): Promise<BankEntity>{
if(!await this.usersService.isUserExists(userId))
throw new NotFoundException("User not found");
const banks = await this.getBanks(userId);
for(const bank of banks)
if(bank.name === bankName)
throw new NotFoundException("Bank already exists");
const bank: BankEntity = await this.prismaService.banks.create({
data: {
user_id: userId,
name: this.encryptionService.encryptSymmetric(bankName, this.configService.get("SYMMETRIC_ENCRYPTION_KEY")),
}
});
bank.name = bankName;
return bank;
}

async updateBankName(userId: number, bankId: number, name: string): Promise<BankEntity>{
if(!await this.usersService.isUserExists(userId))
throw new NotFoundException("User not found");
const bank: BankEntity = await this.prismaService.banks.findUnique({
where: {
id: bankId,
user_id: userId,
}
});
if(!bank)
throw new NotFoundException("User bank not found");
const banks = await this.getBanks(userId);
for(const bank of banks)
if(bank.name === name)
throw new NotFoundException("Bank already exists with this name");
await this.prismaService.banks.update({
where: {
id: bankId,
},
data: {
name: this.encryptionService.encryptSymmetric(name, this.configService.get("SYMMETRIC_ENCRYPTION_KEY")),
}
});
bank.name = name;
return bank;
}

async deleteBank(userId: number, bankId: number): Promise<BankEntity>{
if(!await this.usersService.isUserExists(userId))
throw new NotFoundException("User not found");
const bank: BankEntity = await this.prismaService.banks.findUnique({
where: {
id: bankId,
user_id: userId,
}
});
if(!bank)
throw new NotFoundException("User bank not found");
await this.prismaService.banks.delete({
where: {
id: bankId,
}
});
bank.name = this.encryptionService.decryptSymmetric(bank.name, this.configService.get("SYMMETRIC_ENCRYPTION_KEY"));
return bank;
}
}
9 changes: 9 additions & 0 deletions src/banks/models/dto/bank-name.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {ApiProperty} from "@nestjs/swagger";
import {IsNotEmpty, IsString} from "class-validator";

export class BankNameDto{
@ApiProperty()
@IsString()
@IsNotEmpty()
name: string;
}
11 changes: 11 additions & 0 deletions src/banks/models/entities/bank.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {Banks} from "@prisma/client";
import {ApiProperty} from "@nestjs/swagger";

export class BankEntity implements Banks{
@ApiProperty()
id: number;
@ApiProperty()
name: string;
@ApiProperty()
user_id: number;
}
2 changes: 1 addition & 1 deletion src/middlewares/logger.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class LoggerMiddleware implements NestMiddleware{
const path = req.url;
const statusCode = res.statusCode;
const duration = Date.now() - startTime;
const resSize = res.getHeader("Content-Length");
const resSize = res.getHeader("Content-Length") || "N/A";
console.log(`${currentTime} ${httpOrHttps} ${method} ${path} ${statusCode} ${duration}ms ${resSize}`);
});
next();
Expand Down
Loading

0 comments on commit 9c8cf31

Please sign in to comment.