Add registration

This commit is contained in:
2025-12-22 23:02:41 +01:00
parent 5b91dc0bbe
commit 28094bae12
39 changed files with 441 additions and 129 deletions

View File

@@ -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],
})

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

View File

@@ -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<UserDbEntity | null> {
if (!email || typeof email != 'string') return null;
return await Database.repositories.users.findOneBy({ email });
}
}

View File

@@ -0,0 +1,7 @@
import { randomBytes } from 'crypto';
export class HashGenerator {
static generate(length: number) {
return randomBytes(length / 2).toString('hex');
}
}

View File

@@ -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<string> {
return await new Promise((r) =>
bc.hash(password, AUTH_PASSWORD_HASHING_ROUNDS, (e, hash) => r(hash)),
);
}
static async compare(password: string, hash: string): Promise<boolean> {
return await new Promise((r) =>
bc.compare(password, hash, (e, res) => r(res)),
);
}
}

View File

@@ -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();
}
}

View File

@@ -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<UserDbEntity>;
}

View File

@@ -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<BankAccountDbEntity>[];
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -14,9 +14,9 @@ describe('AppController', () => {
appController = app.get<AppController>(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!');
// });
// });
});

View File

@@ -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();
}
}

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {}

View File

@@ -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) {}
}

View File

@@ -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 {}

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class AccountService {}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { AccountModule } from './account/account.module';
@Module({
imports: [AccountModule],
controllers: [],
providers: [],
})
export class FinanceModule {}

View File

@@ -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);
}
}

View File

@@ -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 {}

View File

@@ -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);
}
}