Compare commits

...
Author SHA1 Message Date
Abdalhamid Alhamad ed57ce6e91 feat: working on money requests jounrey 2024-12-18 12:57:23 +03:00
abdalhamid99 33453b193f Merge pull request #13 from HamzaSha1/feat/register-junior-via-qrcode
feat: onboard junior by qrcode
2024-12-15 17:00:42 +03:00
Abdalhamid Alhamad b0972f1a0a feat: onbard junior by qrcode 2024-12-15 16:46:49 +03:00
abdalhamid99 7437403756 Merge pull request #12 from HamzaSha1/feat/saving-goals
feat: working on saving goals journey for juniors
2024-12-15 12:53:27 +03:00
Abdalhamid Alhamad 4d2f6f57f4 feat: working on saving goals jounrey for juniors 2024-12-15 12:44:59 +03:00
Abdalhamid Alhamad 24d990592d fix: fix tasks submission journey 2024-12-12 15:22:04 +03:00
Abdalhamid Alhamad 5b7b7ff689 fix: fix oci bucket name in generating signed url 2024-12-12 13:30:11 +03:00
abdalhamid99 6fccacd085 Merge pull request #11 from HamzaSha1/feat/customer-settings
feat: update customer profile picture and notifications settings
2024-12-12 13:23:28 +03:00
Abdalhamid Alhamad 51fa61dbc6 feat: update customer profile picture and notifications settings 2024-12-12 13:15:47 +03:00
abdalhamid99 4867a5f858 Merge pull request #10 from HamzaSha1/feat/tasks-default-logo
feat: seed default task logos
2024-12-12 11:22:20 +03:00
Abdalhamid Alhamad 687b6a5c6d feat: seed default task logos 2024-12-12 11:14:38 +03:00
abdalhamid99 e6ed1772f7 Merge pull request #9 from HamzaSha1/feat/signed-urls
Feat/signed urls
2024-12-12 10:01:52 +03:00
Abdalhamid Alhamad 1f0a14fee4 fix: fix magic number lint issue 2024-12-12 09:47:49 +03:00
Abdalhamid Alhamad eb70828ae0 Merge branch 'mvp-1' of github.com:HamzaSha1/zod-backend into feat/signed-urls 2024-12-12 09:46:48 +03:00
Abdalhamid Alhamad 220a03cc46 feat: working on signed url for private files 2024-12-12 09:46:38 +03:00
Abdalhamid Alhamad 39b1e76bb5 fix: import tasks migration 2024-12-11 17:49:15 +03:00
abdalhamid99 83fc634d25 Merge pull request #8 from HamzaSha1/feat/tasks
feat: tasks journey
2024-12-11 11:14:21 +03:00
Abdalhamid Alhamad 35b434bc3d fix: fix multiple submissions 2024-12-11 11:09:55 +03:00
Abdalhamid Alhamad 749ee5457f feat: tasks jounrey 2024-12-11 10:27:51 +03:00
abdalhamid99 d539073f29 Merge pull request #7 from HamzaSha1/feat/roles-guard
feat: protecting endpoints by roles
2024-12-10 10:19:32 +03:00
Abdalhamid Alhamad 66e1bb0f28 feat: protecting endpoint by roles 2024-12-10 10:11:47 +03:00
abdalhamid99 577f91b796 Merge pull request #6 from HamzaSha1/feat/junior-theme
feat: set theme for junior users
2024-12-10 09:30:42 +03:00
Abdalhamid Alhamad 7ed37c30e1 feat: set theme for junior users 2024-12-10 09:23:30 +03:00
abdalhamid99 c2f63ccc72 Merge pull request #5 from HamzaSha1/feat/create-juniors
feat: create junior
2024-12-09 13:18:03 +03:00
Abdalhamid Alhamad 970a41c895 feat: create junior 2024-12-09 13:11:18 +03:00
abdalhamid99 3fd29b3905 Merge pull request #4 from HamzaSha1/feat/forget-password
feat:forget password
2024-12-08 13:26:18 +03:00
Abdalhamid Alhamad 7f7fef3f89 fix: fix pipeline 2024-12-08 13:21:46 +03:00
Abdalhamid Alhamad 90ee8023e6 feat:forget password 2024-12-08 13:15:38 +03:00
abdalhamid99 c486d558ad Merge pull request #3 from HamzaSha1/feat/user-registration
Feat/user registration
2024-12-05 11:50:36 +03:00
174 changed files with 6752 additions and 2984 deletions
+3194 -2794
View File
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -28,6 +28,7 @@
"dependencies": {
"@abdalhamid/hello": "^2.0.0",
"@hamid/hello": "file:../libraries/test-package",
"@keyv/redis": "^4.0.2",
"@nestjs-modules/mailer": "^2.0.2",
"@nestjs/axios": "^3.1.2",
"@nestjs/common": "^10.0.0",
@@ -45,6 +46,7 @@
"amqp-connection-manager": "^4.1.14",
"amqplib": "^0.10.4",
"bcrypt": "^5.1.1",
"cacheable": "^1.8.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"google-libphonenumber": "^3.2.39",
@@ -62,9 +64,11 @@
"pg": "^8.13.1",
"pino-http": "^10.3.0",
"pino-pretty": "^13.0.0",
"reflect-metadata": "^0.2.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"typeorm": "^0.3.20"
"typeorm": "^0.3.20",
"typeorm-transactional": "^0.5.0"
},
"devDependencies": {
"@golevelup/ts-jest": "^0.6.0",
@@ -80,15 +84,19 @@
"@types/node": "^20.3.1",
"@types/nodemailer": "^6.4.16",
"@types/passport-jwt": "^4.0.1",
"@types/qrcode": "^1.5.5",
"@types/supertest": "^6.0.0",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.2",
"@typescript-eslint/parser": "^5.59.2",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-security": "^1.7.1",
"i": "^0.3.7",
"jest": "^29.5.0",
"lint-staged": "^13.2.2",
"npm": "^10.9.2",
"prettier": "^2.8.8",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
+33 -3
View File
@@ -4,7 +4,11 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { I18nMiddleware, I18nModule } from 'nestjs-i18n';
import { LoggerModule } from 'nestjs-pino';
import { DataSource } from 'typeorm';
import { addTransactionalDataSource } from 'typeorm-transactional';
import { AuthModule } from './auth/auth.module';
import { CacheModule } from './common/modules/cache/cache.module';
import { LookupModule } from './common/modules/lookup/lookup.module';
import { OtpModule } from './common/modules/otp/otp.module';
import { AllExceptionsFilter, buildI18nValidationExceptionFilter } from './core/filters';
import { buildConfigOptions, buildLoggerOptions, buildTypeormOptions } from './core/module-options';
@@ -13,7 +17,12 @@ import { buildValidationPipe } from './core/pipes';
import { CustomerModule } from './customer/customer.module';
import { migrations } from './db';
import { DocumentModule } from './document/document.module';
import { GuardianModule } from './guardian/guardian.module';
import { HealthModule } from './health/health.module';
import { JuniorModule } from './junior/junior.module';
import { MoneyRequestModule } from './money-request/money-request.module';
import { SavingGoalsModule } from './saving-goals/saving-goals.module';
import { TaskModule } from './task/task.module';
@Module({
controllers: [],
imports: [
@@ -21,19 +30,40 @@ import { HealthModule } from './health/health.module';
TypeOrmModule.forRootAsync({
imports: [],
inject: [ConfigService],
useFactory: (config: ConfigService) => buildTypeormOptions(config, migrations),
useFactory: (config: ConfigService) => {
return buildTypeormOptions(config, migrations);
},
/* eslint-disable require-await */
async dataSourceFactory(options) {
if (!options) {
throw new Error('Invalid options passed');
}
return addTransactionalDataSource(new DataSource(options));
},
/* eslint-enable require-await */
}),
LoggerModule.forRootAsync({
useFactory: (config: ConfigService) => buildLoggerOptions(config),
inject: [ConfigService],
}),
I18nModule.forRoot(buildI18nOptions()),
CacheModule,
// App modules
AuthModule,
CustomerModule,
DocumentModule,
HealthModule,
JuniorModule,
TaskModule,
GuardianModule,
SavingGoalsModule,
MoneyRequestModule,
OtpModule,
DocumentModule,
LookupModule,
HealthModule,
],
providers: [
// Global Pipes
+10 -3
View File
@@ -1,15 +1,22 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CustomerModule } from '~/customer/customer.module';
import { JuniorModule } from '~/junior/junior.module';
import { AuthController } from './controllers';
import { Device, User, UserNotificationSettings } from './entities';
import { Device, User } from './entities';
import { DeviceRepository, UserRepository } from './repositories';
import { AuthService, DeviceService } from './services';
import { UserService } from './services/user.service';
import { AccessTokenStrategy } from './strategies';
@Module({
imports: [TypeOrmModule.forFeature([User, UserNotificationSettings, Device]), JwtModule.register({})],
imports: [
TypeOrmModule.forFeature([User, Device]),
JwtModule.register({}),
forwardRef(() => CustomerModule),
forwardRef(() => JuniorModule),
],
providers: [AuthService, UserRepository, UserService, DeviceService, DeviceRepository, AccessTokenStrategy],
controllers: [AuthController],
exports: [UserService],
+2
View File
@@ -1 +1,3 @@
export * from './country-code-regex.constant.';
export * from './passcode-regext.constant';
export * from './password-regex.constant';
@@ -0,0 +1 @@
export const PASSCODE_REGEX = /^\d{6}$/;
@@ -0,0 +1 @@
export const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[@$!%*?&#\-_])[A-Za-z\d@$!%*?&#\-_]{8,}$/;
+24 -2
View File
@@ -1,19 +1,22 @@
import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { DEVICE_ID_HEADER } from '~/common/constants';
import { AuthenticatedUser } from '~/common/decorators';
import { AuthenticatedUser, Public } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { ResponseFactory } from '~/core/utils';
import {
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
LoginRequestDto,
SendForgetPasswordOtpRequestDto,
SetEmailRequestDto,
setJuniorPasswordRequestDto,
SetPasscodeRequestDto,
VerifyUserRequestDto,
} from '../dtos/request';
import { SendRegisterOtpResponseDto } from '../dtos/response';
import { SendForgetPasswordOtpResponseDto, SendRegisterOtpResponseDto } from '../dtos/response';
import { LoginResponseDto } from '../dtos/response/login.response.dto';
import { IJwtPayload } from '../interfaces';
import { AuthService } from '../services';
@@ -63,6 +66,25 @@ export class AuthController {
return this.authService.disableBiometric(sub, disableBiometricDto);
}
@Post('forget-password/otp')
async forgetPassword(@Body() sendForgetPasswordOtpDto: SendForgetPasswordOtpRequestDto) {
const email = await this.authService.sendForgetPasswordOtp(sendForgetPasswordOtpDto);
return ResponseFactory.data(new SendForgetPasswordOtpResponseDto(email));
}
@Post('forget-password/reset')
@HttpCode(HttpStatus.NO_CONTENT)
resetPassword(@Body() forgetPasswordDto: ForgetPasswordRequestDto) {
return this.authService.verifyForgetPasswordOtp(forgetPasswordDto);
}
@Post('junior/set-passcode')
@HttpCode(HttpStatus.NO_CONTENT)
@Public()
setJuniorPasscode(@Body() setPasscodeDto: setJuniorPasswordRequestDto) {
return this.authService.setJuniorPasscode(setPasscodeDto);
}
@Post('login')
async login(@Body() loginDto: LoginRequestDto, @Headers(DEVICE_ID_HEADER) deviceId: string) {
const [res, user] = await this.authService.login(loginDto, deviceId);
@@ -0,0 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsNumberString, IsString, MaxLength, MinLength } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { DEFAULT_OTP_LENGTH } from '~/common/modules/otp/constants';
export class ForgetPasswordRequestDto {
@ApiProperty({ example: '[email protected]' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
@ApiProperty({ example: 'password' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.password' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.password' }) })
password!: string;
@ApiProperty({ example: 'password' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.confirmPassword' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.confirmPassword' }) })
confirmPassword!: string;
@ApiProperty({ example: '111111' })
@IsNumberString(
{ no_symbols: true },
{ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.otp' }) },
)
@MaxLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MaxLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
@MinLength(DEFAULT_OTP_LENGTH, {
message: i18n('validation.MinLength', { path: 'general', property: 'auth.otp', length: DEFAULT_OTP_LENGTH }),
})
otp!: string;
}
+3
View File
@@ -1,7 +1,10 @@
export * from './create-unverified-user.request.dto';
export * from './disable-biometric.request.dto';
export * from './enable-biometric.request.dto';
export * from './forget-password.request.dto';
export * from './login.request.dto';
export * from './send-forget-password-otp.request.dto';
export * from './set-email.request.dto';
export * from './set-junior-password.request.dto';
export * from './set-passcode.request.dto';
export * from './verify-user.request.dto';
@@ -0,0 +1,4 @@
import { PickType } from '@nestjs/swagger';
import { LoginRequestDto } from './login.request.dto';
export class SendForgetPasswordOtpRequestDto extends PickType(LoginRequestDto, ['email']) {}
@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { SetPasscodeRequestDto } from './set-passcode.request.dto';
export class setJuniorPasswordRequestDto extends SetPasscodeRequestDto {
@ApiProperty()
@IsString({ message: i18n('validation.IsNumberString', { path: 'general', property: 'auth.qrToken' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.qrToken' }) })
qrToken!: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from './send-forget-password.response.dto';
export * from './send-register-otp.response.dto';
export * from './user.response.dto';
export * from './verify-user.response.dto';
@@ -0,0 +1,7 @@
export class SendForgetPasswordOtpResponseDto {
email!: string;
constructor(email: string) {
this.email = email;
}
}
-1
View File
@@ -1,3 +1,2 @@
export * from './device.entity';
export * from './user-notification-settings.entity';
export * from './user.entity';
-16
View File
@@ -3,7 +3,6 @@ import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
@@ -11,10 +10,8 @@ import {
} from 'typeorm';
import { Otp } from '~/common/modules/otp/entities';
import { Customer } from '~/customer/entities/customer.entity';
import { Document } from '~/document/entities';
import { Roles } from '../enums';
import { Device } from './device.entity';
import { UserNotificationSettings } from './user-notification-settings.entity';
@Entity('users')
export class User extends BaseEntity {
@@ -48,22 +45,9 @@ export class User extends BaseEntity {
@Column('text', { nullable: true, array: true, name: 'roles' })
roles!: Roles[];
@Column('varchar', { name: 'profile_picture_id', nullable: true })
profilePictureId!: string;
@OneToOne(() => Document, (document) => document.user, { cascade: true, nullable: true })
@JoinColumn({ name: 'profile_picture_id' })
profilePicture!: Document;
@OneToMany(() => Otp, (otp) => otp.user)
otp!: Otp[];
@OneToOne(() => UserNotificationSettings, (notificationSettings) => notificationSettings.user, {
cascade: true,
eager: true,
})
notificationSettings!: UserNotificationSettings;
@OneToOne(() => Customer, (customer) => customer.user, { cascade: true, eager: true })
customer!: Customer;
+3 -1
View File
@@ -1,4 +1,6 @@
import { Roles } from '../enums';
export interface IJwtPayload {
sub: string;
roles: string[];
roles: Roles[];
}
+10 -16
View File
@@ -1,9 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
import { Customer } from '~/customer/entities';
import { User, UserNotificationSettings } from '../entities';
import { User } from '../entities';
@Injectable()
export class UserRepository {
@@ -15,27 +13,23 @@ export class UserRepository {
phoneNumber: data.phoneNumber,
countryCode: data.countryCode,
roles: data.roles,
notificationSettings: UserNotificationSettings.create(),
}),
);
}
findOne(where: FindOptionsWhere<User>) {
findOne(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
return this.userRepository.findOne({ where });
}
updateNotificationSettings(user: User, body: UpdateNotificationsSettingsRequestDto) {
user.notificationSettings = UserNotificationSettings.create({ ...user.notificationSettings, ...body });
return this.userRepository.save(user);
}
verifyUserAndCreateCustomer(user: User) {
user.customer = Customer.create({ ...user.customer, id: user.id, isGuardian: true });
return this.userRepository.save(user);
}
update(userId: string, data: Partial<User>) {
return this.userRepository.update(userId, data);
}
createUser(data: Partial<User>) {
const user = this.userRepository.create({
...data,
});
return this.userRepository.save(user);
}
}
+70 -2
View File
@@ -4,16 +4,21 @@ import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { OtpScope, OtpType } from '~/common/modules/otp/enums';
import { OtpService } from '~/common/modules/otp/services';
import { JuniorTokenService } from '~/junior/services';
import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants';
import {
CreateUnverifiedUserRequestDto,
DisableBiometricRequestDto,
EnableBiometricRequestDto,
ForgetPasswordRequestDto,
LoginRequestDto,
SendForgetPasswordOtpRequestDto,
SetEmailRequestDto,
setJuniorPasswordRequestDto,
} from '../dtos/request';
import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto';
import { User } from '../entities';
import { GrantType } from '../enums';
import { GrantType, Roles } from '../enums';
import { ILoginResponse } from '../interfaces';
import { removePadding, verifySignature } from '../utils';
import { DeviceService } from './device.service';
@@ -29,13 +34,14 @@ export class AuthService {
private readonly configService: ConfigService,
private readonly userService: UserService,
private readonly deviceService: DeviceService,
private readonly juniorTokenService: JuniorTokenService,
) {}
async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) {
const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode });
return this.otpService.generateAndSendOtp({
userId: user.id,
phoneNumber: user.phoneNumber,
recipient: user.phoneNumber,
scope: OtpScope.VERIFY_PHONE,
otpType: OtpType.SMS,
});
@@ -126,6 +132,44 @@ export class AuthService {
return this.deviceService.updateDevice(deviceId, { publicKey: null });
}
async sendForgetPasswordOtp({ email }: SendForgetPasswordOtpRequestDto) {
const user = await this.userService.findUserOrThrow({ email });
if (!user.isProfileCompleted) {
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
}
return this.otpService.generateAndSendOtp({
userId: user.id,
recipient: user.email,
scope: OtpScope.FORGET_PASSWORD,
otpType: OtpType.EMAIL,
});
}
async verifyForgetPasswordOtp({ email, otp, password, confirmPassword }: ForgetPasswordRequestDto) {
const user = await this.userService.findUserOrThrow({ email });
if (!user.isProfileCompleted) {
throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED');
}
const isOtpValid = await this.otpService.verifyOtp({
userId: user.id,
scope: OtpScope.FORGET_PASSWORD,
otpType: OtpType.EMAIL,
value: otp,
});
if (!isOtpValid) {
throw new BadRequestException('USERS.INVALID_OTP');
}
this.validatePassword(password, confirmPassword, user);
const hashedPassword = bcrypt.hashSync(password, user.salt);
await this.userService.setPasscode(user.id, hashedPassword, user.salt);
}
async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> {
const user = await this.userService.findUser({ email: loginDto.email });
let tokens;
@@ -145,6 +189,14 @@ export class AuthService {
return [tokens, user];
}
async setJuniorPasscode(body: setJuniorPasswordRequestDto) {
const juniorId = await this.juniorTokenService.validateToken(body.qrToken);
const salt = bcrypt.genSaltSync(SALT_ROUNDS);
const hashedPasscode = bcrypt.hashSync(body.passcode, salt);
await this.userService.setPasscode(juniorId, hashedPasscode, salt);
await this.juniorTokenService.invalidateToken(body.qrToken);
}
private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise<ILoginResponse> {
const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password);
@@ -205,4 +257,20 @@ export class AuthService {
return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) };
}
private validatePassword(password: string, confirmPassword: string, user: User) {
if (password !== confirmPassword) {
throw new BadRequestException('AUTH.PASSWORD_MISMATCH');
}
const roles = user.roles;
if (roles.includes(Roles.GUARDIAN) && !PASSCODE_REGEX.test(password)) {
throw new BadRequestException('AUTH.INVALID_PASSCODE');
}
if (roles.includes(Roles.JUNIOR) && !PASSWORD_REGEX.test(password)) {
throw new BadRequestException('AUTH.INVALID_PASSWORD');
}
}
}
+26 -15
View File
@@ -1,6 +1,8 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm';
import { UpdateNotificationsSettingsRequestDto } from '~/customer/dtos/request';
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
import { CustomerService } from '~/customer/services';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { CreateUnverifiedUserRequestDto } from '../dtos/request';
import { User } from '../entities';
import { Roles } from '../enums';
@@ -8,18 +10,12 @@ import { UserRepository } from '../repositories';
@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}
constructor(
private readonly userRepository: UserRepository,
@Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService,
) {}
async updateNotificationSettings(userId: string, body: UpdateNotificationsSettingsRequestDto) {
const user = await this.findUserOrThrow({ id: userId });
const notificationSettings = (await this.userRepository.updateNotificationSettings(user, body))
.notificationSettings;
return notificationSettings;
}
findUser(where: FindOptionsWhere<User>) {
findUser(where: FindOptionsWhere<User> | FindOptionsWhere<User>[]) {
return this.userRepository.findOne(where);
}
@@ -44,12 +40,19 @@ export class UserService {
}
if (user && user.roles.includes(Roles.JUNIOR)) {
throw new BadRequestException('USERS.JUNIOR_UPGRADE_NOT_SUPPORTED_YET');
//TODO add role Guardian to the existing user and send OTP
}
return user;
}
async createUser(data: Partial<User>) {
const user = await this.userRepository.createUser(data);
return user;
}
setEmail(userId: string, email: string) {
return this.userRepository.update(userId, { email });
}
@@ -58,7 +61,15 @@ export class UserService {
return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true });
}
verifyUserAndCreateCustomer(user: User) {
return this.userRepository.verifyUserAndCreateCustomer(user);
async verifyUserAndCreateCustomer(user: User) {
await this.customerService.createCustomer(
{
guardian: Guardian.create({ id: user.id }),
notificationSettings: new CustomerNotificationSettings(),
},
user,
);
return this.findUserOrThrow({ id: user.id });
}
}
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { Roles } from '~/auth/enums';
export const ROLE_METADATA_KEY = 'roles';
export const AllowedRoles = (...roles: Roles[]) => SetMetadata(ROLE_METADATA_KEY, roles);
+1
View File
@@ -1,2 +1,3 @@
export * from './allowed-roles.decorator';
export * from './public.decorator';
export * from './user.decorator';
+1 -1
View File
@@ -6,7 +6,7 @@ import { IS_PUBLIC_KEY } from '../decorators';
@Injectable()
export class AccessTokenGuard extends AuthGuard('access-token') {
constructor(private reflector: Reflector) {
constructor(protected reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
+1
View File
@@ -1 +1,2 @@
export * from './access-token.guard';
export * from './roles-guard';
+28
View File
@@ -0,0 +1,28 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Roles } from '~/auth/enums';
import { ROLE_METADATA_KEY } from '../decorators';
import { AccessTokenGuard } from './access-token.guard';
@Injectable()
export class RolesGuard extends AccessTokenGuard {
async canActivate(context: ExecutionContext): Promise<boolean> {
await super.canActivate(context);
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false;
}
const allowedRoles = this.reflector.getAllAndOverride<Roles[]>(ROLE_METADATA_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!allowedRoles) {
return true;
}
return allowedRoles.some((role) => user.roles.includes(role));
}
}
+17
View File
@@ -0,0 +1,17 @@
import { Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { buildKeyvOptions } from '~/core/module-options';
import { CacheService } from './services';
@Module({
providers: [
{
provide: 'CACHE_INSTANCE',
useFactory: (config: ConfigService) => buildKeyvOptions(config),
inject: [ConfigService],
},
CacheService,
],
exports: ['CACHE_INSTANCE', CacheService],
})
export class CacheModule {}
+19
View File
@@ -0,0 +1,19 @@
import { Inject, Injectable } from '@nestjs/common';
import { Cacheable } from 'cacheable';
@Injectable()
export class CacheService {
constructor(@Inject('CACHE_INSTANCE') private readonly cache: Cacheable) {}
get<T>(key: string): Promise<T | undefined> {
return this.cache.get(key);
}
async set<T>(key: string, value: T, ttl?: number | string): Promise<void> {
await this.cache.set(key, value, ttl);
}
async delete(key: string): Promise<void> {
await this.cache.delete(key);
}
}
+1
View File
@@ -0,0 +1 @@
export * from './cache.services';
@@ -0,0 +1 @@
export * from './lookup.controller';
@@ -0,0 +1,32 @@
import { Controller, Get, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AccessTokenGuard } from '~/common/guards';
import { ApiDataArrayResponse } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import { DocumentMetaResponseDto } from '~/document/dtos/response';
import { LookupService } from '../services';
@Controller('lookup')
@ApiTags('Lookups')
@ApiBearerAuth()
export class LookupController {
constructor(private readonly lookupService: LookupService) {}
@UseGuards(AccessTokenGuard)
@Get('default-avatars')
@ApiDataArrayResponse(DocumentMetaResponseDto)
async findDefaultAvatars() {
const avatars = await this.lookupService.findDefaultAvatar();
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
}
@UseGuards(AccessTokenGuard)
@Get('default-task-logos')
@ApiDataArrayResponse(DocumentMetaResponseDto)
async findDefaultTaskLogos() {
const avatars = await this.lookupService.findDefaultTasksLogo();
return ResponseFactory.dataArray(avatars.map((avatar) => new DocumentMetaResponseDto(avatar)));
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { DocumentModule } from '~/document/document.module';
import { LookupController } from './controllers';
import { LookupService } from './services';
@Module({
controllers: [LookupController],
providers: [LookupService],
imports: [DocumentModule],
})
export class LookupModule {}
@@ -0,0 +1 @@
export * from './lookup.service';
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { DocumentType } from '~/document/enums';
import { DocumentService, OciService } from '~/document/services';
@Injectable()
export class LookupService {
constructor(private readonly documentService: DocumentService, private readonly ociService: OciService) {}
async findDefaultAvatar() {
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR });
await Promise.all(
documents.map(async (document) => {
document.url = await this.ociService.generatePreSignedUrl(document);
}),
);
return documents;
}
async findDefaultTasksLogo() {
const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_TASKS_LOGO });
await Promise.all(
documents.map(async (document) => {
document.url = await this.ociService.generatePreSignedUrl(document);
}),
);
return documents;
}
}
@@ -1,20 +0,0 @@
import { IsNotEmpty } from 'class-validator';
import { UserLocale } from '~/core/enums';
import { OtpScope, OtpType } from '../../enums';
export class GenerateOtpRequestDto {
@IsNotEmpty()
userId!: string;
@IsNotEmpty()
phoneNumber!: string;
@IsNotEmpty()
scope!: OtpScope;
@IsNotEmpty()
language?: UserLocale = UserLocale.ENGLISH;
@IsNotEmpty()
otpType!: OtpType;
}
@@ -1,2 +0,0 @@
export * from './generate-otp-request.request.dto';
export * from './verify-otp-request.dto';
@@ -1,16 +0,0 @@
import { IsNotEmpty } from 'class-validator';
import { OtpScope, OtpType } from '../../enums';
export class VerifyOtpRequestDto {
@IsNotEmpty()
userId!: string;
@IsNotEmpty()
scope!: OtpScope;
@IsNotEmpty()
otpType!: OtpType;
@IsNotEmpty()
value!: string;
}
@@ -1,3 +1,4 @@
export enum OtpScope {
VERIFY_PHONE = 'VERIFY_PHONE',
FORGET_PASSWORD = 'FORGET_PASSWORD',
}
@@ -0,0 +1,2 @@
export * from './send-otp.interface';
export * from './verify-otp.interface';
@@ -0,0 +1,9 @@
import { OtpScope, OtpType } from '../enums';
export interface ISendOtp {
userId: string;
scope: OtpScope;
language?: string;
otpType: OtpType;
recipient: string;
}
@@ -0,0 +1,8 @@
import { OtpScope, OtpType } from '../enums';
export interface IVerifyOtp {
userId: string;
scope: OtpScope;
otpType: OtpType;
value: string;
}
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { VerifyOtpRequestDto } from '../dtos/request';
import { Otp } from '../entities';
import { IVerifyOtp } from '../interfaces';
const FIVE = 5;
const SIXTY = 60;
const ONE_THOUSAND = 1000;
@@ -23,7 +23,7 @@ export class OtpRepository {
);
}
findOtp(otp: VerifyOtpRequestDto) {
findOtp(otp: IVerifyOtp) {
return this.otpRepository.findOne({
where: {
userId: otp.userId,
@@ -1,7 +1,8 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants';
import { GenerateOtpRequestDto, VerifyOtpRequestDto } from '../dtos/request';
import { OtpType } from '../enums';
import { ISendOtp, IVerifyOtp } from '../interfaces';
import { OtpRepository } from '../repositories';
import { generateRandomOtp } from '../utils';
@@ -9,23 +10,25 @@ import { generateRandomOtp } from '../utils';
export class OtpService {
constructor(private readonly configService: ConfigService, private readonly otpRepository: OtpRepository) {}
private useMock = this.configService.get<boolean>('USE_MOCK', false);
async generateAndSendOtp(sendotpRequest: GenerateOtpRequestDto) {
async generateAndSendOtp(sendotpRequest: ISendOtp): Promise<string> {
const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH);
await this.otpRepository.createOtp({ ...sendotpRequest, value: otp });
this.sendOtp(sendotpRequest, otp);
return sendotpRequest.phoneNumber.replace(/.(?=.{4})/g, '*');
return sendotpRequest.otpType == OtpType.EMAIL
? sendotpRequest.recipient
: sendotpRequest.recipient?.replace(/.(?=.{4})/g, '*');
}
async verifyOtp(verifyOtpRequest: VerifyOtpRequestDto) {
async verifyOtp(verifyOtpRequest: IVerifyOtp) {
const otp = await this.otpRepository.findOtp(verifyOtpRequest);
return !!otp;
}
private sendOtp(sendotpRequest: GenerateOtpRequestDto, otp: string) {
private sendOtp(sendotpRequest: ISendOtp, otp: string) {
// TODO: send OTP to the user
return;
}
+2 -1
View File
@@ -1,3 +1,4 @@
export * from '././keyv-options';
export * from './config-options';
export * from './typeorm-options';
export * from './logger-options';
export * from './typeorm-options';
+8
View File
@@ -0,0 +1,8 @@
import KeyvRedis from '@keyv/redis';
import { ConfigService } from '@nestjs/config';
import { Cacheable } from 'cacheable';
export function buildKeyvOptions(config: ConfigService) {
const secondary = new KeyvRedis(config.get('REDIS_URL'));
return new Cacheable({ secondary, ttl: config.get('REDIS_TTL') });
}
+1
View File
@@ -0,0 +1 @@
export * from './object-values.type';
+13
View File
@@ -0,0 +1,13 @@
/**
* Converts an object's values to a union type.
* Should be used in conjunction with constant objects to define enums.
*
* @example
* const Status = { SUCCEEDED: 'Success', FAILED: 'Failure' } as const;
* type Status = ObjectValues<typeof Status>; // 'Success' | 'Failure'
* const foo: Status = Status.SUCCEEDED;
* const bar: Status = 'Failure';
*
* @see https://youtu.be/jjMbPt_H3RQ
*/
export type ObjectValues<T> = T[keyof T];
@@ -1,8 +1,9 @@
import { Body, Controller, Patch, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { IJwtPayload } from '~/auth/interfaces';
import { AuthenticatedUser } from '~/common/decorators';
import { AccessTokenGuard } from '~/common/guards';
import { ApiDataResponse } from '~/core/decorators';
import { ResponseFactory } from '~/core/utils';
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { CustomerResponseDto, NotificationSettingsResponseDto } from '../dtos/response';
@@ -11,11 +12,21 @@ import { CustomerService } from '../services';
@Controller('customers')
@ApiTags('Customers')
@ApiBearerAuth()
@UseGuards(AccessTokenGuard)
export class CustomerController {
constructor(private readonly customerService: CustomerService) {}
@Get('/profile')
@UseGuards(AccessTokenGuard)
@ApiDataResponse(CustomerResponseDto)
async getCustomerProfile(@AuthenticatedUser() { sub }: IJwtPayload) {
const customer = await this.customerService.findCustomerById(sub);
return ResponseFactory.data(new CustomerResponseDto(customer));
}
@Patch('')
@UseGuards(AccessTokenGuard)
@ApiDataResponse(CustomerResponseDto)
async updateCustomer(@AuthenticatedUser() { sub }: IJwtPayload, @Body() body: UpdateCustomerRequestDto) {
const customer = await this.customerService.updateCustomer(sub, body);
@@ -23,6 +34,8 @@ export class CustomerController {
}
@Patch('settings/notifications')
@UseGuards(AccessTokenGuard)
@ApiDataResponse(NotificationSettingsResponseDto)
async updateNotificationSettings(
@AuthenticatedUser() { sub }: IJwtPayload,
@Body() body: UpdateNotificationsSettingsRequestDto,
+4 -2
View File
@@ -1,14 +1,16 @@
import { Module } from '@nestjs/common';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from '~/auth/auth.module';
import { CustomerController } from './controllers';
import { Customer } from './entities';
import { CustomerNotificationSettings } from './entities/customer-notification-settings.entity';
import { CustomerRepository } from './repositories/customer.repository';
import { CustomerService } from './services';
@Module({
imports: [TypeOrmModule.forFeature([Customer]), AuthModule],
imports: [TypeOrmModule.forFeature([Customer, CustomerNotificationSettings]), forwardRef(() => AuthModule)],
controllers: [CustomerController],
providers: [CustomerService, CustomerRepository],
exports: [CustomerService],
})
export class CustomerModule {}
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { IsDateString, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { IsAbove18 } from '~/core/decorators/validations';
export class UpdateCustomerRequestDto {
@@ -26,4 +26,8 @@ export class UpdateCustomerRequestDto {
@IsAbove18({ message: i18n('validation.IsAbove18', { path: 'general', property: 'customer.dateOfBirth' }) })
@IsOptional()
dateOfBirth!: Date;
@ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' })
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) })
profilePictureId!: string;
}
@@ -1,5 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Customer } from '~/customer/entities';
import { DocumentMetaResponseDto } from '~/document/dtos/response';
import { NotificationSettingsResponseDto } from './notification-settings.response.dto';
export class CustomerResponseDto {
@ApiProperty()
@@ -50,6 +52,12 @@ export class CustomerResponseDto {
@ApiProperty()
isGuardian!: boolean;
@ApiProperty()
notificationSettings!: NotificationSettingsResponseDto;
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
profilePicture!: DocumentMetaResponseDto | null;
constructor(customer: Customer) {
this.id = customer.id;
this.customerStatus = customer.customerStatus;
@@ -67,5 +75,7 @@ export class CustomerResponseDto {
this.gender = customer.gender;
this.isJunior = customer.isJunior;
this.isGuardian = customer.isGuardian;
this.notificationSettings = new NotificationSettingsResponseDto(customer.notificationSettings);
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
}
}
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { UserNotificationSettings } from '~/auth/entities';
import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity';
export class NotificationSettingsResponseDto {
@ApiProperty()
@@ -11,7 +11,7 @@ export class NotificationSettingsResponseDto {
@ApiProperty()
isSmsEnabled!: boolean;
constructor(notificationSettings: UserNotificationSettings) {
constructor(notificationSettings: CustomerNotificationSettings) {
this.isEmailEnabled = notificationSettings.isEmailEnabled;
this.isPushEnabled = notificationSettings.isPushEnabled;
this.isSmsEnabled = notificationSettings.isSmsEnabled;
@@ -8,10 +8,10 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from './user.entity';
import { Customer } from '~/customer/entities';
@Entity('user_notification_settings')
export class UserNotificationSettings extends BaseEntity {
@Entity('cutsomer_notification_settings')
export class CustomerNotificationSettings extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@@ -24,9 +24,9 @@ export class UserNotificationSettings extends BaseEntity {
@Column({ name: 'is_sms_enabled', default: false })
isSmsEnabled!: boolean;
@OneToOne(() => User, (user) => user.notificationSettings, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@OneToOne(() => Customer, (customer) => customer.notificationSettings, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'customer_id' })
customer!: Customer;
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
createdAt!: Date;
+24 -1
View File
@@ -9,8 +9,12 @@ import {
UpdateDateColumn,
} from 'typeorm';
import { User } from '~/auth/entities';
import { Document } from '~/document/entities';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { Junior } from '~/junior/entities';
import { CustomerNotificationSettings } from './customer-notification-settings.entity';
@Entity()
@Entity('customers')
export class Customer extends BaseEntity {
@PrimaryColumn('uuid')
id!: string;
@@ -63,10 +67,29 @@ export class Customer extends BaseEntity {
@Column('varchar', { name: 'user_id' })
userId!: string;
@Column('varchar', { name: 'profile_picture_id', nullable: true })
profilePictureId!: string;
@OneToOne(() => CustomerNotificationSettings, (notificationSettings) => notificationSettings.customer, {
cascade: true,
eager: true,
})
notificationSettings!: CustomerNotificationSettings;
@OneToOne(() => Document, (document) => document.customerPicture, { cascade: true, nullable: true })
@JoinColumn({ name: 'profile_picture_id' })
profilePicture!: Document;
@OneToOne(() => User, (user) => user.customer, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user!: User;
@OneToOne(() => Junior, (junior) => junior.customer, { cascade: true })
junior!: Junior;
@OneToOne(() => Guardian, (guardian) => guardian.customer, { cascade: true })
guardian!: Guardian;
@CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@@ -1,18 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UpdateCustomerRequestDto } from '../dtos/request';
import { User } from '~/auth/entities';
import { Roles } from '~/auth/enums';
import { UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { Customer } from '../entities';
import { CustomerNotificationSettings } from '../entities/customer-notification-settings.entity';
@Injectable()
export class CustomerRepository {
constructor(@InjectRepository(Customer) private readonly customerRepository: Repository<Customer>) {}
updateCustomer(id: string, data: UpdateCustomerRequestDto) {
updateCustomer(id: string, data: Partial<Customer>) {
return this.customerRepository.update(id, data);
}
findOne(where: FindOptionsWhere<Customer>) {
return this.customerRepository.findOne({ where });
return this.customerRepository.findOne({ where, relations: ['profilePicture'] });
}
createCustomer(customerData: Partial<Customer>, user: User) {
return this.customerRepository.save(
this.customerRepository.create({
...customerData,
id: user.id,
user,
isGuardian: user.roles.includes(Roles.GUARDIAN),
isJunior: user.roles.includes(Roles.JUNIOR),
}),
);
}
updateNotificationSettings(customer: Customer, body: UpdateNotificationsSettingsRequestDto) {
customer.notificationSettings = CustomerNotificationSettings.create({
...customer.notificationSettings,
...body,
});
return this.customerRepository.save(customer);
}
}
+19 -4
View File
@@ -1,14 +1,20 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { UserService } from '~/auth/services/user.service';
import { User } from '~/auth/entities';
import { OciService } from '~/document/services';
import { UpdateCustomerRequestDto, UpdateNotificationsSettingsRequestDto } from '../dtos/request';
import { Customer } from '../entities';
import { CustomerRepository } from '../repositories/customer.repository';
@Injectable()
export class CustomerService {
constructor(private readonly userService: UserService, private readonly customerRepository: CustomerRepository) {}
updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
return this.userService.updateNotificationSettings(userId, data);
constructor(private readonly customerRepository: CustomerRepository, private readonly ociService: OciService) {}
async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto) {
const customer = await this.findCustomerById(userId);
const notificationSettings = (await this.customerRepository.updateNotificationSettings(customer, data))
.notificationSettings;
return notificationSettings;
}
async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise<Customer> {
@@ -16,11 +22,20 @@ export class CustomerService {
return this.findCustomerById(userId);
}
createCustomer(customerData: Partial<Customer>, user: User) {
return this.customerRepository.createCustomer(customerData, user);
}
async findCustomerById(id: string) {
const customer = await this.customerRepository.findOne({ id });
if (!customer) {
throw new BadRequestException('CUSTOMER.NOT_FOUND');
}
if (customer.profilePicture) {
customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture);
}
return customer;
}
}
@@ -9,7 +9,7 @@ export class CreateDocumentEntity1732434281561 implements MigrationInterface {
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" character varying(255) NOT NULL,
"extension" character varying(255) NOT NULL,
"documentType" character varying(255) NOT NULL,
"document_type" character varying(255) NOT NULL,
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_ac51aa5181ee2036f5ca482857c" PRIMARY KEY ("id"))`,
@@ -16,16 +16,10 @@ export class CreateUserEntity1733206728721 implements MigrationInterface {
"apple_id" character varying(255),
"is_profile_completed" boolean NOT NULL DEFAULT false,
"roles" text array,
"profile_picture_id" uuid,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "FK_02ec15de199e79a0c46869895f4" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
@@ -1,29 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateNotificationSettingsTable1733231692252 implements MigrationInterface {
name = 'CreateNotificationSettingsTable1733231692252';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "user_notification_settings"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"is_email_enabled" boolean NOT NULL DEFAULT false,
"is_push_enabled" boolean NOT NULL DEFAULT false,
"is_sms_enabled" boolean NOT NULL DEFAULT false,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"user_id" uuid, CONSTRAINT "REL_52182ffd0f785e8256f8fcb4fd" UNIQUE ("user_id"),
CONSTRAINT "PK_a195de67d093e096152f387afbd" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "user_notification_settings" ADD CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user_notification_settings" DROP CONSTRAINT "FK_52182ffd0f785e8256f8fcb4fd6"`,
);
await queryRunner.query(`DROP TABLE "user_notification_settings"`);
}
}
@@ -5,7 +5,7 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "customer"
`CREATE TABLE "customers"
("id" uuid NOT NULL,
"customer_status" character varying(255) NOT NULL DEFAULT 'PENDING',
"rejection_reason" text,
@@ -22,19 +22,25 @@ export class CreateCustomerEntity1733298524771 implements MigrationInterface {
"gender" character varying(255),
"is_junior" boolean NOT NULL DEFAULT false,
"is_guardian" boolean NOT NULL DEFAULT false,
"profile_picture_id" uuid,
"user_id" uuid NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
CONSTRAINT "REL_5d1f609371a285123294fddcf3" UNIQUE ("user_id"),
CONSTRAINT "REL_02ec15de199e79a0c46869895f" UNIQUE ("profile_picture_id"),
CONSTRAINT "PK_a7a13f4cacb744524e44dfdad32" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "customer" ADD CONSTRAINT "FK_5d1f609371a285123294fddcf3a" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
`ALTER TABLE "customers" ADD CONSTRAINT "FK_e7574892da11dd01de5cfc46499" FOREIGN KEY ("profile_picture_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "customers" ADD CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "customer" DROP CONSTRAINT "FK_5d1f609371a285123294fddcf3a"`);
await queryRunner.query(`DROP TABLE "customer"`);
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_11d81cd7be87b6f8865b0cf7661"`);
await queryRunner.query(`ALTER TABLE "customers" DROP CONSTRAINT "FK_e7574892da11dd01de5cfc46499"`);
await queryRunner.query(`DROP TABLE "customers"`);
}
}
@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateJuniorEntity1733731507261 implements MigrationInterface {
name = 'CreateJuniorEntity1733731507261';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "juniors"
("id" uuid NOT NULL,
"relationship" character varying(255) NOT NULL,
"civil_id_front_id" uuid NOT NULL,
"civil_id_back_id" uuid NOT NULL,
"customer_id" uuid NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "REL_6a72e1a5758643737cc563b96c" UNIQUE ("civil_id_front_id"),
CONSTRAINT "REL_4662c4433223c01fe69fc1382f" UNIQUE ("civil_id_back_id"),
CONSTRAINT "REL_dfbf64ede1ff823a489902448a" UNIQUE ("customer_id"), CONSTRAINT "PK_2d273092322c1f8bf26296fa608" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_6a72e1a5758643737cc563b96c7" FOREIGN KEY ("civil_id_front_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_4662c4433223c01fe69fc1382f5" FOREIGN KEY ("civil_id_back_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_dfbf64ede1ff823a489902448a2" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_dfbf64ede1ff823a489902448a2"`);
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_4662c4433223c01fe69fc1382f5"`);
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_6a72e1a5758643737cc563b96c7"`);
await queryRunner.query(`DROP TABLE "juniors"`);
}
}
@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateGuardianEntity1733732021622 implements MigrationInterface {
name = 'CreateGuardianEntity1733732021622';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "guardians"
("id" uuid NOT NULL,
"customer_id" uuid NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "REL_6c46a1b6af00e6457cb1b70f7e" UNIQUE ("customer_id"), CONSTRAINT "PK_3dcf02f3dc96a2c017106f280be" PRIMARY KEY ("id"))`,
);
await queryRunner.query(`ALTER TABLE "juniors" ADD "guardian_id" uuid NOT NULL`);
await queryRunner.query(
`ALTER TABLE "juniors" ADD CONSTRAINT "FK_0b11aa56264184690e2220da4a0" FOREIGN KEY ("guardian_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "guardians" ADD CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "guardians" DROP CONSTRAINT "FK_6c46a1b6af00e6457cb1b70f7e7"`);
await queryRunner.query(`ALTER TABLE "juniors" DROP CONSTRAINT "FK_0b11aa56264184690e2220da4a0"`);
await queryRunner.query(`ALTER TABLE "juniors" DROP COLUMN "guardian_id"`);
await queryRunner.query(`DROP TABLE "guardians"`);
}
}
@@ -0,0 +1,28 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateThemeEntity1733748083604 implements MigrationInterface {
name = 'CreateThemeEntity1733748083604';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "themes"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"color" character varying(255) NOT NULL,
"avatar_id" uuid, "junior_id" uuid NOT NULL,
CONSTRAINT "REL_73fcb76399a308cdd2d431a8f2" UNIQUE ("junior_id"),
CONSTRAINT "PK_ddbeaab913c18682e5c88155592" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "themes" ADD CONSTRAINT "FK_169b672cc28cc757e1f4464864d" FOREIGN KEY ("avatar_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "themes" ADD CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_73fcb76399a308cdd2d431a8f2e"`);
await queryRunner.query(`ALTER TABLE "themes" DROP CONSTRAINT "FK_169b672cc28cc757e1f4464864d"`);
await queryRunner.query(`DROP TABLE "themes"`);
}
}
@@ -0,0 +1,73 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { Document } from '../../document/entities';
import { DocumentType } from '../../document/enums';
const DEFAULT_AVATARS = [
{
id: uuid(),
name: 'vacation',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'colors',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'astronaut',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'pet',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'disney',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'clothes',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'playstation',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'football',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
{
id: uuid(),
name: 'cars',
extension: '.jpg',
documentType: DocumentType.DEFAULT_AVATAR,
},
];
export class SeedDefaultAvatar1733750228289 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager.getRepository(Document).save(DEFAULT_AVATARS);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await DEFAULT_AVATARS.forEach(async (avatar) => {
await queryRunner.manager
.getRepository(Document)
.delete({ name: avatar.name, documentType: avatar.documentType });
});
}
}
@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateTaskEntities1733904556416 implements MigrationInterface {
name = 'CreateTaskEntities1733904556416';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "task_submissions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "status" character varying NOT NULL, "submitted_at" TIMESTAMP WITH TIME ZONE NOT NULL, "task_id" uuid NOT NULL, "proof_of_completion_id" uuid, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "REL_d6cfaee118a0300d652e28ee16" UNIQUE ("task_id"), CONSTRAINT "PK_8d19d6b5dd776e373113de50018" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "tasks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "title" character varying(255) NOT NULL, "description" character varying(255) NOT NULL, "reward_amount" numeric(12,3) NOT NULL, "image_id" uuid NOT NULL, "task_frequency" character varying NOT NULL, "start_date" date NOT NULL, "due_date" date NOT NULL, "is_proof_required" boolean NOT NULL, "assigned_to_id" uuid NOT NULL, "assigned_by_id" uuid NOT NULL, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_8d12ff38fcc62aaba2cab748772" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "task_submissions" ADD CONSTRAINT "FK_d6cfaee118a0300d652e28ee166" FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "task_submissions" ADD CONSTRAINT "FK_87876dfe440de7aafce216e9f58" FOREIGN KEY ("proof_of_completion_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_f1f00d41b1e95d0bbda2710a62b" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_9430f12c5a1604833f64595a57f" FOREIGN KEY ("assigned_to_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "tasks" ADD CONSTRAINT "FK_3e08a7ca125a175cf899b09f71a" FOREIGN KEY ("assigned_by_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_3e08a7ca125a175cf899b09f71a"`);
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_9430f12c5a1604833f64595a57f"`);
await queryRunner.query(`ALTER TABLE "tasks" DROP CONSTRAINT "FK_f1f00d41b1e95d0bbda2710a62b"`);
await queryRunner.query(`ALTER TABLE "task_submissions" DROP CONSTRAINT "FK_87876dfe440de7aafce216e9f58"`);
await queryRunner.query(`ALTER TABLE "task_submissions" DROP CONSTRAINT "FK_d6cfaee118a0300d652e28ee166"`);
await queryRunner.query(`DROP TABLE "tasks"`);
await queryRunner.query(`DROP TABLE "task_submissions"`);
}
}
@@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { v4 as uuid } from 'uuid';
import { Document } from '~/document/entities';
import { DocumentType } from '~/document/enums';
const DEFAULT_TASK_LOGOS = [
{
id: uuid(),
name: 'bed-furniture',
extension: '.jpg',
documentType: DocumentType.DEFAULT_TASKS_LOGO,
},
{
id: uuid(),
name: 'dog',
extension: '.jpg',
documentType: DocumentType.DEFAULT_TASKS_LOGO,
},
{
id: uuid(),
name: 'dish-washing',
extension: '.jpg',
documentType: DocumentType.DEFAULT_TASKS_LOGO,
},
{
id: uuid(),
name: 'walking-the-dog',
extension: '.jpg',
documentType: DocumentType.DEFAULT_TASKS_LOGO,
},
];
export class SeedsDefaultTasksLogo1733990253208 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager.getRepository(Document).save(DEFAULT_TASK_LOGOS);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await DEFAULT_TASK_LOGOS.forEach(async (logo) => {
await queryRunner.manager.getRepository(Document).delete({ name: logo.name, documentType: logo.documentType });
});
}
}
@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateCustomerNotificationsSettingsTable1733993920226 implements MigrationInterface {
name = 'CreateCustomerNotificationsSettingsTable1733993920226';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "cutsomer_notification_settings"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"is_email_enabled" boolean NOT NULL DEFAULT false,
"is_push_enabled" boolean NOT NULL DEFAULT false,
"is_sms_enabled" boolean NOT NULL DEFAULT false,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"customer_id" uuid, CONSTRAINT "REL_32f2b707407298a9eecd6cc7ea" UNIQUE ("customer_id"),
CONSTRAINT "PK_ea94fb22410c89ae6b37d63b0e3" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "cutsomer_notification_settings" ADD CONSTRAINT "FK_32f2b707407298a9eecd6cc7ea6" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "cutsomer_notification_settings" DROP CONSTRAINT "FK_32f2b707407298a9eecd6cc7ea6"`,
);
await queryRunner.query(`DROP TABLE "cutsomer_notification_settings"`);
}
}
@@ -0,0 +1,70 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSavingGoalsEntities1734246386471 implements MigrationInterface {
name = 'CreateSavingGoalsEntities1734246386471';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "saving_goals"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" character varying(255) NOT NULL,
"description" character varying(255),
"due_date" date NOT NULL,
"target_amount" numeric(12,3) NOT NULL,
"current_amount" numeric(12,3) NOT NULL DEFAULT '0',
"image_id" uuid, "junior_id" uuid NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_5193f14c1c3a38e6657a159795e" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "categories"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" character varying(255) NOT NULL,
"type" character varying(255) NOT NULL,
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
"junior_id" uuid, CONSTRAINT "PK_24dbc6126a28ff948da33e97d3b" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "saving_goals_categories"
("saving_goal_id" uuid NOT NULL,
"category_id" uuid NOT NULL,
CONSTRAINT "PK_a49d4f57d06d0a36a8385b6c28f" PRIMARY KEY ("saving_goal_id", "category_id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_d421de423f21c01672ea7c2e98" ON "saving_goals_categories" ("saving_goal_id") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_b0a721a8f7f5b6fe93f3603ebc" ON "saving_goals_categories" ("category_id") `,
);
await queryRunner.query(
`ALTER TABLE "saving_goals" ADD CONSTRAINT "FK_dad35932272342c1a247a2cee1c" FOREIGN KEY ("image_id") REFERENCES "documents"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "saving_goals" ADD CONSTRAINT "FK_f494ba0a361b2f9cbe5f6e56e38" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "categories" ADD CONSTRAINT "FK_4f98e0b010d5e90cae0a2007748" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "saving_goals_categories" ADD CONSTRAINT "FK_d421de423f21c01672ea7c2e98f" FOREIGN KEY ("saving_goal_id") REFERENCES "saving_goals"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
);
await queryRunner.query(
`ALTER TABLE "saving_goals_categories" ADD CONSTRAINT "FK_b0a721a8f7f5b6fe93f3603ebc8" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "saving_goals_categories" DROP CONSTRAINT "FK_b0a721a8f7f5b6fe93f3603ebc8"`);
await queryRunner.query(`ALTER TABLE "saving_goals_categories" DROP CONSTRAINT "FK_d421de423f21c01672ea7c2e98f"`);
await queryRunner.query(`ALTER TABLE "categories" DROP CONSTRAINT "FK_4f98e0b010d5e90cae0a2007748"`);
await queryRunner.query(`ALTER TABLE "saving_goals" DROP CONSTRAINT "FK_f494ba0a361b2f9cbe5f6e56e38"`);
await queryRunner.query(`ALTER TABLE "saving_goals" DROP CONSTRAINT "FK_dad35932272342c1a247a2cee1c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_b0a721a8f7f5b6fe93f3603ebc"`);
await queryRunner.query(`DROP INDEX "public"."IDX_d421de423f21c01672ea7c2e98"`);
await queryRunner.query(`DROP TABLE "saving_goals_categories"`);
await queryRunner.query(`DROP TABLE "categories"`);
await queryRunner.query(`DROP TABLE "saving_goals"`);
}
}
@@ -0,0 +1,57 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { CategoryType } from '~/saving-goals/enums';
const DEFAULT_CATEGORIES = [
{
name: 'School',
type: CategoryType.GLOBAL,
},
{
name: 'Toys',
type: CategoryType.GLOBAL,
},
{
name: 'Games',
type: CategoryType.GLOBAL,
},
{
name: 'Clothes',
type: CategoryType.GLOBAL,
},
{
name: 'Hobbies',
type: CategoryType.GLOBAL,
},
{
name: 'Party',
type: CategoryType.GLOBAL,
},
{
name: 'Sport',
type: CategoryType.GLOBAL,
},
{
name: 'University',
type: CategoryType.GLOBAL,
},
{
name: 'Travel',
type: CategoryType.GLOBAL,
},
];
export class SeedsGoalsCategories1734247702310 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`INSERT INTO categories (name, type) VALUES ${DEFAULT_CATEGORIES.map(
(category) => `('${category.name}', '${category.type}')`,
)}`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM categories WHERE name IN (${DEFAULT_CATEGORIES.map(
(category) => `'${category.name}'`,
)}) AND type = '${CategoryType.GLOBAL}'`,
);
}
}
@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateJuniorRegistrationTokenTable1734262619426 implements MigrationInterface {
name = 'CreateJuniorRegistrationTokenTable1734262619426';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "junior_registration_tokens"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"token" character varying(255) NOT NULL,
"is_used" boolean NOT NULL DEFAULT false,
"expiry_date" TIMESTAMP NOT NULL,
"junior_id" uuid NOT NULL,
"updated_at" TIMESTAMP NOT NULL DEFAULT now(),
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "UQ_e6a3e23d5a63be76812dc5d7728" UNIQUE ("token"),
CONSTRAINT "PK_610992ebec8f664113ae48b946f" PRIMARY KEY ("id"))`,
);
await queryRunner.query(`CREATE INDEX "IDX_e6a3e23d5a63be76812dc5d772" ON "junior_registration_tokens" ("token") `);
await queryRunner.query(
`ALTER TABLE "junior_registration_tokens" ADD CONSTRAINT "FK_19c52317b5b7aeecc0a11789b53" FOREIGN KEY ("junior_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "junior_registration_tokens" DROP CONSTRAINT "FK_19c52317b5b7aeecc0a11789b53"`,
);
await queryRunner.query(`DROP INDEX "public"."IDX_e6a3e23d5a63be76812dc5d772"`);
await queryRunner.query(`DROP TABLE "junior_registration_tokens"`);
}
}
@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateMoneyRequestEntity1734503895302 implements MigrationInterface {
name = 'CreateMoneyRequestEntity1734503895302';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "money_requests"
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"requested_amount" numeric(10,3) NOT NULL,
"message" character varying NOT NULL,
"frequency" character varying NOT NULL DEFAULT 'ONE_TIME',
"status" character varying NOT NULL DEFAULT 'PENDING',
"reviewed_at" TIMESTAMP WITH TIME ZONE,
"end_date" TIMESTAMP WITH TIME ZONE,
"requester_id" uuid NOT NULL,
"reviewer_id" uuid NOT NULL,
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT "PK_28cff23e9fb06cd5dbf73cd53e7" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_5cce02836c6033b6e2412995e34" FOREIGN KEY ("requester_id") REFERENCES "juniors"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "money_requests" ADD CONSTRAINT "FK_75ba0766db9a7bf03126facf31c" FOREIGN KEY ("reviewer_id") REFERENCES "guardians"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_75ba0766db9a7bf03126facf31c"`);
await queryRunner.query(`ALTER TABLE "money_requests" DROP CONSTRAINT "FK_5cce02836c6033b6e2412995e34"`);
await queryRunner.query(`DROP TABLE "money_requests"`);
}
}
+11 -1
View File
@@ -1,6 +1,16 @@
export * from './1732434281561-create-document-entity';
export * from './1733206728721-create-user-entity';
export * from './1733209041336-create-otp-entity';
export * from './1733231692252-create-notification-settings-table';
export * from './1733298524771-create-customer-entity';
export * from './1733314952318-create-device-entity';
export * from './1733731507261-create-junior-entity';
export * from './1733732021622-create-guardian-entity';
export * from './1733748083604-create-theme-entity';
export * from './1733750228289-seed-default-avatar';
export * from './1733904556416-create-task-entities';
export * from './1733990253208-seeds-default-tasks-logo';
export * from './1733993920226-create-customer-notifications-settings-table';
export * from './1734246386471-create-saving-goals-entities';
export * from './1734247702310-seeds-goals-categories';
export * from './1734262619426-create-junior-registration-token-table';
export * from './1734503895302-create-money-request-entity';
@@ -3,4 +3,9 @@ import { DocumentType } from '../enums';
export const BUCKETS: Record<DocumentType, string> = {
[DocumentType.PROFILE_PICTURE]: 'profile-pictures',
[DocumentType.PASSPORT]: 'passports',
[DocumentType.DEFAULT_AVATAR]: 'avatars',
[DocumentType.DEFAULT_TASKS_LOGO]: 'tasks-logo',
[DocumentType.CUSTOM_AVATAR]: 'avatars',
[DocumentType.CUSTOM_TASKS_LOGO]: 'tasks-logo',
[DocumentType.GOALS]: 'goals',
};
@@ -24,7 +24,9 @@ export class DocumentController {
},
documentType: {
type: 'string',
enum: Object.values(DocumentType),
enum: Object.values(DocumentType).filter(
(value) => ![DocumentType.DEFAULT_AVATAR, DocumentType.DEFAULT_TASKS_LOGO].includes(value),
),
},
},
required: ['document', 'documentType'],
+5 -2
View File
@@ -1,13 +1,16 @@
import { Module } from '@nestjs/common';
import { Global, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CacheModule } from '~/common/modules/cache/cache.module';
import { DocumentController } from './controllers';
import { Document } from './entities';
import { DocumentRepository } from './repositories';
import { DocumentService, OciService } from './services';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([Document])],
imports: [TypeOrmModule.forFeature([Document]), CacheModule],
controllers: [DocumentController],
providers: [DocumentService, OciService, DocumentRepository],
exports: [DocumentService, OciService],
})
export class DocumentModule {}
@@ -15,18 +15,14 @@ export class DocumentMetaResponseDto {
@ApiProperty()
documentType!: DocumentType;
@ApiProperty()
createdAt!: Date;
@ApiProperty()
updatedAt!: Date;
@ApiProperty({ type: String })
url!: string | null;
constructor(document: Document) {
this.id = document.id;
this.name = document.name;
this.extension = document.extension;
this.documentType = document.documentType;
this.createdAt = document.createdAt;
this.updatedAt = document.updatedAt;
this.url = document.url || null;
}
}
+30 -4
View File
@@ -1,5 +1,10 @@
import { Column, Entity, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { Column, Entity, OneToMany, OneToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { User } from '~/auth/entities';
import { Customer } from '~/customer/entities';
import { Junior, Theme } from '~/junior/entities';
import { SavingGoal } from '~/saving-goals/entities';
import { Task } from '~/task/entities';
import { TaskSubmission } from '~/task/entities/task-submissions.entity';
import { DocumentType } from '../enums';
@Entity('documents')
@@ -13,15 +18,36 @@ export class Document {
@Column({ type: 'varchar', length: 255 })
extension!: string;
@Column({ type: 'varchar', length: 255 })
@Column({ type: 'varchar', length: 255, name: 'document_type' })
documentType!: DocumentType;
@OneToOne(() => User, (user) => user.profilePicture, { onDelete: 'CASCADE' })
user!: User;
@OneToOne(() => Customer, (customer) => customer.profilePicture, { onDelete: 'SET NULL' })
customerPicture?: Customer;
@OneToOne(() => Junior, (junior) => junior.civilIdFront, { onDelete: 'SET NULL' })
juniorCivilIdFront?: User;
@OneToOne(() => Junior, (junior) => junior.civilIdBack, { onDelete: 'SET NULL' })
juniorCivilIdBack?: User;
@OneToMany(() => Theme, (theme) => theme.avatar)
themes?: Theme[];
@OneToMany(() => Task, (task) => task.image)
tasks?: Task[];
@OneToMany(() => TaskSubmission, (taskSubmission) => taskSubmission.proofOfCompletion)
submissions?: TaskSubmission[];
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.image)
goals?: SavingGoal[];
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
@Column({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
// virtual field
url?: string;
}
+5
View File
@@ -1,4 +1,9 @@
export enum DocumentType {
PROFILE_PICTURE = 'PROFILE_PICTURE',
PASSPORT = 'PASSPORT',
DEFAULT_AVATAR = 'DEFAULT_AVATAR',
DEFAULT_TASKS_LOGO = 'DEFAULT_TASKS_LOGO',
CUSTOM_AVATAR = 'CUSTOM_AVATAR',
CUSTOM_TASKS_LOGO = 'CUSTOM_TASKS_LOGO',
GOALS = 'GOALS',
}
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { FindOptionsWhere, Repository } from 'typeorm';
import { UploadResponseDto } from '../dtos/response';
import { Document } from '../entities';
@@ -17,4 +17,8 @@ export class DocumentRepository {
}),
);
}
findDocuments(where: FindOptionsWhere<Document>) {
return this.documentRepository.find({ where });
}
}
@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { FindOptionsWhere } from 'typeorm';
import { UploadDocumentRequestDto } from '../dtos/request';
import { Document } from '../entities';
import { DocumentRepository } from '../repositories';
import { OciService } from './oci.service';
@@ -10,4 +12,8 @@ export class DocumentService {
const uploadedFile = await this.ociService.uploadFile(file, uploadedDocumentRequest);
return this.documentRepository.createDocument(uploadedFile);
}
findDocuments(where: FindOptionsWhere<Document>) {
return this.documentRepository.findDocuments(where);
}
}
+42 -2
View File
@@ -1,14 +1,18 @@
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { plainToInstance } from 'class-transformer';
import moment from 'moment';
import { Region, SimpleAuthenticationDetailsProvider } from 'oci-common';
import { ObjectStorageClient } from 'oci-objectstorage';
import { CreatePreauthenticatedRequestDetails } from 'oci-objectstorage/lib/model';
import path from 'path';
import { CacheService } from '~/common/modules/cache/services';
import { BUCKETS } from '../constants';
import { UploadDocumentRequestDto } from '../dtos/request';
import { UploadResponseDto } from '../dtos/response';
import { Document } from '../entities';
import { generateNewFileName } from '../utils';
const TWO = 2;
@Injectable()
export class OciService {
private readonly ociClient: ObjectStorageClient;
@@ -20,7 +24,7 @@ export class OciService {
private readonly namespace: string = this.configService.getOrThrow('OCI_NAMESPACE');
private readonly region: string = this.configService.getOrThrow('OCI_REGION');
private readonly logger = new Logger(OciService.name);
constructor(private configService: ConfigService) {
constructor(private configService: ConfigService, private readonly cacheService: CacheService) {
this.ociClient = new ObjectStorageClient({
authenticationDetailsProvider: new SimpleAuthenticationDetailsProvider(
this.tenancyId,
@@ -60,4 +64,40 @@ export class OciService {
documentType,
});
}
async generatePreSignedUrl(document?: Document): Promise<string | any> {
if (!document) {
return null;
}
const cachedUrl = await this.cacheService.get<string>(document.id);
if (cachedUrl) {
return cachedUrl;
}
const bucketName = BUCKETS[document.documentType];
const objectName = document.name;
const expiration = moment().add(TWO, 'hours').toDate();
try {
this.logger.debug(`Generating pre-signed url for object ${objectName} in bucket ${bucketName}`);
const res = await this.ociClient.createPreauthenticatedRequest({
namespaceName: this.namespace,
bucketName,
createPreauthenticatedRequestDetails: {
name: objectName,
accessType: CreatePreauthenticatedRequestDetails.AccessType.AnyObjectRead,
timeExpires: expiration,
objectName,
},
retryConfiguration: { terminationStrategy: { shouldTerminate: () => true } },
});
this.cacheService.set(document.id, res.preauthenticatedRequest.fullPath + objectName, '1h');
return res.preauthenticatedRequest.fullPath + objectName;
} catch (error) {
this.logger.error(`Error generating pre-signed url: ${error}`);
return document.name;
}
}
}
View File
+43
View File
@@ -0,0 +1,43 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
import { Customer } from '~/customer/entities';
import { Junior } from '~/junior/entities';
import { MoneyRequest } from '~/money-request/entities';
import { Task } from '~/task/entities';
@Entity('guardians')
export class Guardian extends BaseEntity {
@PrimaryColumn('uuid')
id!: string;
@Column('uuid', { name: 'customer_id' })
customerId!: string;
@OneToOne(() => Customer, (customer) => customer.guardian, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'customer_id' })
customer!: Customer;
@OneToMany(() => Junior, (junior) => junior.guardian)
juniors!: Junior[];
@OneToMany(() => Task, (task) => task.assignedBy)
tasks?: Task[];
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.reviewer)
moneyRequests?: MoneyRequest[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
}
+8
View File
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Guardian } from './entities/guradian.entity';
@Module({
imports: [TypeOrmModule.forFeature([Guardian])],
})
export class GuardianModule {}
+1
View File
@@ -0,0 +1 @@
export * from './junior.controller';
@@ -0,0 +1,88 @@
import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Roles } from '~/auth/enums';
import { IJwtPayload } from '~/auth/interfaces';
import { AllowedRoles, AuthenticatedUser, Public } from '~/common/decorators';
import { RolesGuard } from '~/common/guards';
import { ApiDataPageResponse, ApiDataResponse } from '~/core/decorators';
import { PageOptionsRequestDto } from '~/core/dtos';
import { CustomParseUUIDPipe } from '~/core/pipes';
import { ResponseFactory } from '~/core/utils';
import { CreateJuniorRequestDto, SetThemeRequestDto } from '../dtos/request';
import { JuniorResponseDto, QrCodeValidationResponseDto, ThemeResponseDto } from '../dtos/response';
import { JuniorService } from '../services';
@Controller('juniors')
@ApiTags('Juniors')
@ApiBearerAuth()
export class JuniorController {
constructor(private readonly juniorService: JuniorService) {}
@Post()
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(JuniorResponseDto)
async createJunior(@Body() body: CreateJuniorRequestDto, @AuthenticatedUser() user: IJwtPayload) {
const token = await this.juniorService.createJuniors(body, user.sub);
return ResponseFactory.data(token);
}
@Get()
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataPageResponse(JuniorResponseDto)
async findJuniors(@AuthenticatedUser() user: IJwtPayload, @Query() pageOptions: PageOptionsRequestDto) {
const [juniors, count] = await this.juniorService.findJuniorsByGuardianId(user.sub, pageOptions);
return ResponseFactory.dataPage(
juniors.map((juniors) => new JuniorResponseDto(juniors)),
{
page: pageOptions.page,
size: pageOptions.size,
itemCount: count,
},
);
}
@Get(':juniorId')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse(JuniorResponseDto)
async findJuniorById(
@AuthenticatedUser() user: IJwtPayload,
@Param('juniorId', CustomParseUUIDPipe) juniorId: string,
) {
const junior = await this.juniorService.findJuniorById(juniorId, false, user.sub);
return ResponseFactory.data(new JuniorResponseDto(junior));
}
@Post('set-theme')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.JUNIOR)
@ApiDataResponse(JuniorResponseDto)
async setTheme(@Body() body: SetThemeRequestDto, @AuthenticatedUser() user: IJwtPayload) {
const theme = await this.juniorService.setTheme(body, user.sub);
return ResponseFactory.data(new ThemeResponseDto(theme));
}
@Get(':juniorId/qr-code')
@UseGuards(RolesGuard)
@AllowedRoles(Roles.GUARDIAN)
@ApiDataResponse('string')
async generateQrCode(@Param('juniorId', CustomParseUUIDPipe) juniorId: string) {
const qrCode = await this.juniorService.generateToken(juniorId);
return ResponseFactory.data(qrCode);
}
@Get('qr-code/:token/validate')
@Public()
@ApiDataResponse(QrCodeValidationResponseDto)
async validateToken(@Param('token') token: string) {
const junior = await this.juniorService.validateToken(token);
return ResponseFactory.data(new QrCodeValidationResponseDto(junior));
}
}
@@ -0,0 +1,37 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, IsEmail, IsNotEmpty, IsString, Matches } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { COUNTRY_CODE_REGEX } from '~/auth/constants';
import { IsValidPhoneNumber } from '~/core/decorators/validations';
export class CreateJuniorUserRequestDto {
@ApiProperty({ example: '+962' })
@Matches(COUNTRY_CODE_REGEX, {
message: i18n('validation.Matches', { path: 'general', property: 'auth.countryCode' }),
})
countryCode: string = '+966';
@ApiProperty({ example: '787259134' })
@IsValidPhoneNumber({
message: i18n('validation.IsValidPhoneNumber', { path: 'general', property: 'auth.phoneNumber' }),
})
phoneNumber!: string;
@ApiProperty({ example: 'John' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.firstName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.firstName' }) })
firstName!: string;
@ApiProperty({ example: 'Doe' })
@IsString({ message: i18n('validation.IsString', { path: 'general', property: 'auth.lastName' }) })
@IsNotEmpty({ message: i18n('validation.IsNotEmpty', { path: 'general', property: 'auth.lastName' }) })
lastName!: string;
@ApiProperty({ example: '2020-01-01' })
@IsDateString({}, { message: i18n('validation.IsDateString', { path: 'general', property: 'customer.dateOfBirth' }) })
dateOfBirth!: Date;
@ApiProperty({ example: '[email protected]' })
@IsEmail({}, { message: i18n('validation.IsEmail', { path: 'general', property: 'auth.email' }) })
email!: string;
}
@@ -0,0 +1,18 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsUUID } from 'class-validator';
import { i18nValidationMessage as i18n } from 'nestjs-i18n';
import { Relationship } from '~/junior/enums';
import { CreateJuniorUserRequestDto } from './create-junior-user.request.dto';
export class CreateJuniorRequestDto extends CreateJuniorUserRequestDto {
@ApiProperty({ example: Relationship.PARENT })
@IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) })
relationship!: Relationship;
@ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' })
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdFrontId' }) })
civilIdFrontId!: string;
@ApiProperty({ example: 'bf342-3f3f-3f3f-3f3f' })
@IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'junior.civilIdBackId' }) })
civilIdBackId!: string;
}
+3
View File
@@ -0,0 +1,3 @@
export * from './create-junior-user.request.dto';
export * from './create-junior.request.dto';
export * from './set-theme.request.dto';
@@ -0,0 +1,13 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsUUID } from 'class-validator';
import { ThemeColor } from '~/junior/enums';
export class SetThemeRequestDto {
@ApiProperty({ example: ThemeColor.VIOLET })
@IsEnum(ThemeColor)
color!: ThemeColor;
@ApiProperty({ example: 'fbfre-4f4f-4f4f-4f4f' })
@IsUUID()
avatarId!: string;
}
+4
View File
@@ -0,0 +1,4 @@
export * from './junior.response.dto';
export * from './qr-code-validation-details.response.dto';
export * from './qr-code-validation.response.dto';
export * from './theme.response.dto';
@@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger';
import { DocumentMetaResponseDto } from '~/document/dtos/response';
import { Junior } from '~/junior/entities';
import { Relationship } from '~/junior/enums';
export class JuniorResponseDto {
@ApiProperty({ example: 'id' })
id!: string;
@ApiProperty({ example: 'fullName' })
fullName!: string;
@ApiProperty({ example: 'relationship' })
relationship!: Relationship;
@ApiProperty({ type: DocumentMetaResponseDto })
profilePicture!: DocumentMetaResponseDto | null;
constructor(junior: Junior) {
this.id = junior.id;
this.fullName = `${junior.customer.firstName} ${junior.customer.lastName}`;
this.relationship = junior.relationship;
this.profilePicture = junior.customer.profilePicture
? new DocumentMetaResponseDto(junior.customer.profilePicture)
: null;
}
}
@@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { Junior } from '~/junior/entities';
import { GuardianRelationship } from '~/junior/enums';
export class QrCodeValidationDetailsResponse {
@ApiProperty()
fullname!: string;
@ApiProperty()
phoneNumber!: string;
@ApiProperty()
email!: string;
@ApiProperty()
relationship?: string;
@ApiProperty()
dateOfBirth!: Date;
constructor(junior: Junior, guardian?: Guardian) {
const person = guardian ? guardian : junior;
this.fullname = `${person.customer.firstName} ${person.customer.lastName}`;
this.phoneNumber = person.customer.user.phoneNumber;
this.email = person.customer.user.email;
this.dateOfBirth = person.customer.dateOfBirth;
this.relationship = guardian ? junior.relationship : GuardianRelationship[junior.relationship];
}
}
@@ -0,0 +1,16 @@
import { ApiProperty } from '@nestjs/swagger';
import { Junior } from '~/junior/entities';
import { QrCodeValidationDetailsResponse } from './qr-code-validation-details.response.dto';
export class QrCodeValidationResponseDto {
@ApiProperty({ type: QrCodeValidationDetailsResponse })
parentDetails: any;
@ApiProperty({ type: QrCodeValidationDetailsResponse })
childDetails: any;
constructor(junior: Junior) {
this.parentDetails = new QrCodeValidationDetailsResponse(junior, junior.guardian);
this.childDetails = new QrCodeValidationDetailsResponse(junior);
}
}
@@ -0,0 +1,26 @@
import { ApiProperty } from '@nestjs/swagger';
import { Theme } from '~/junior/entities';
import { ThemeColor } from '~/junior/enums';
export class ThemeResponseDto {
@ApiProperty({ example: '645fds-4f5s4f-4f5s4f-4f5s4f' })
id!: string;
@ApiProperty({ example: ThemeColor.BLUE })
color!: string;
@ApiProperty({ example: '645fds-4f5s4f-4f5s4f-4f5s4f' })
avatarId!: string;
@ApiProperty({ example: '645fds-4f5s4f-4f5s4f-4f5s4f' })
juniorId!: string;
constructor(theme: Theme | null) {
if (theme) {
this.id = theme.id;
this.color = theme.color;
this.avatarId = theme.avatarId;
this.juniorId = theme.juniorId;
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export * from './junior-registration-token.entity';
export * from './junior.entity';
export * from './theme.entity';
@@ -0,0 +1,41 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Junior } from './junior.entity';
@Entity('junior_registration_tokens')
export class JuniorRegistrationToken extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'varchar', length: 255, name: 'token', unique: true })
@Index()
token!: string;
@Column({ type: 'boolean', default: false, name: 'is_used' })
isUsed!: boolean;
@Column({ type: 'timestamp', name: 'expiry_date' })
expiryDate!: Date;
@Column({ type: 'uuid', name: 'junior_id' })
juniorId!: string;
@ManyToOne(() => Junior, (junior) => junior.registrationTokens)
@JoinColumn({ name: 'junior_id' })
junior!: Junior;
@UpdateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', name: 'updated_at' })
updatedAt!: Date;
@CreateDateColumn({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' })
createdAt!: Date;
}
+82
View File
@@ -0,0 +1,82 @@
import {
BaseEntity,
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
PrimaryColumn,
UpdateDateColumn,
} from 'typeorm';
import { Customer } from '~/customer/entities';
import { Document } from '~/document/entities';
import { Guardian } from '~/guardian/entities/guradian.entity';
import { MoneyRequest } from '~/money-request/entities';
import { Category, SavingGoal } from '~/saving-goals/entities';
import { Task } from '~/task/entities';
import { Relationship } from '../enums';
import { JuniorRegistrationToken } from './junior-registration-token.entity';
import { Theme } from './theme.entity';
@Entity('juniors')
export class Junior extends BaseEntity {
@PrimaryColumn('uuid')
id!: string;
@Column('varchar', { length: 255 })
relationship!: Relationship;
@Column('uuid', { name: 'civil_id_front_id' })
civilIdFrontId!: string;
@Column('uuid', { name: 'civil_id_back_id' })
civilIdBackId!: string;
@Column('uuid', { name: 'customer_id' })
customerId!: string;
@Column('uuid', { name: 'guardian_id' })
guardianId!: string;
@OneToOne(() => Document, (document) => document.juniorCivilIdFront)
@JoinColumn({ name: 'civil_id_front_id' })
civilIdFront!: Document;
@OneToOne(() => Document, (document) => document.juniorCivilIdBack)
@JoinColumn({ name: 'civil_id_back_id' })
civilIdBack!: Document;
@OneToOne(() => Customer, (customer) => customer.junior, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'customer_id' })
customer!: Customer;
@OneToOne(() => Theme, (theme) => theme.junior, { cascade: true, nullable: true })
theme!: Theme;
@ManyToOne(() => Guardian, (guardian) => guardian.juniors)
@JoinColumn({ name: 'guardian_id' })
guardian!: Guardian;
@OneToMany(() => Task, (task) => task.assignedTo)
tasks!: Task[];
@OneToMany(() => SavingGoal, (savingGoal) => savingGoal.junior)
goals!: SavingGoal[];
@OneToMany(() => Category, (category) => category.junior)
categories!: Category[];
@OneToMany(() => JuniorRegistrationToken, (token) => token.junior)
registrationTokens!: JuniorRegistrationToken[];
@OneToMany(() => MoneyRequest, (moneyRequest) => moneyRequest.requester)
moneyRequests!: MoneyRequest[];
@CreateDateColumn({ name: 'created_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
updatedAt!: Date;
}
+26
View File
@@ -0,0 +1,26 @@
import { BaseEntity, Column, Entity, JoinColumn, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Document } from '~/document/entities';
import { Junior } from './junior.entity';
@Entity('themes')
export class Theme extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column('varchar', { length: 255, name: 'color' })
color!: string;
@Column('uuid', { name: 'avatar_id', nullable: true })
avatarId!: string;
@ManyToOne(() => Document, (document) => document.themes, { cascade: true, nullable: true })
@JoinColumn({ name: 'avatar_id' })
avatar!: Document;
@Column('uuid', { name: 'junior_id' })
juniorId!: string;
@OneToOne(() => Junior, (junior) => junior.theme, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'junior_id' })
junior!: Junior;
}
@@ -0,0 +1,9 @@
import { ObjectValues } from '~/core/types';
import { Relationship } from './relationship.enum';
export const GuardianRelationship = {
[Relationship.PARENT]: 'CHILD',
[Relationship.GUARDIAN]: 'WARD',
} as const;
export type GuardianRelationship = ObjectValues<typeof GuardianRelationship>;
+3
View File
@@ -0,0 +1,3 @@
export * from './guardian-relationship.enum';
export * from './relationship.enum';
export * from './theme-color.enum';

Some files were not shown because too many files have changed in this diff Show More