Compare commits
No commits in common. "custom-starter-app" and "main" have entirely different histories.
custom-sta
...
main
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,8 +1,5 @@
|
|||||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dotenv
|
|
||||||
.env
|
|
||||||
|
|
||||||
# compiled output
|
# compiled output
|
||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"printWidth": 120,
|
"singleQuote": true
|
||||||
"singleQuote": true,
|
|
||||||
"plugins": ["prettier-plugin-organize-imports"]
|
|
||||||
}
|
}
|
||||||
|
20
.vscode/launch.json
vendored
20
.vscode/launch.json
vendored
@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Attach to ebitemp-api",
|
|
||||||
"port": 9229,
|
|
||||||
"request": "attach",
|
|
||||||
"restart": true,
|
|
||||||
"skipFiles": [
|
|
||||||
"<node_internals>/**",
|
|
||||||
"${workspaceFolder}/node_modules/**/*.js"
|
|
||||||
],
|
|
||||||
"internalConsoleOptions": "neverOpen",
|
|
||||||
"type": "node"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"pluralizeNames": true,
|
|
||||||
"noConfigs": true,
|
|
||||||
"convertCaseFile": "none",
|
|
||||||
"convertCaseEntity": "pascal",
|
|
||||||
"convertCaseProperty": "camel",
|
|
||||||
"convertEol": "LF",
|
|
||||||
"propertyVisibility": "none",
|
|
||||||
"lazy": false,
|
|
||||||
"activeRecord": false,
|
|
||||||
"generateConstructor": true,
|
|
||||||
"customNamingStrategyPath": ".tomg-naming-strategy.js",
|
|
||||||
"relationIds": false,
|
|
||||||
"strictMode": "!",
|
|
||||||
"skipSchema": true,
|
|
||||||
"indexFile": false,
|
|
||||||
"exportType": "named",
|
|
||||||
"skipNonPrimaryKeyIndexes": true,
|
|
||||||
"removeColumnsInRelation": false
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,39 +0,0 @@
|
|||||||
/**
|
|
||||||
* @typedef {import('typeorm-model-generator').Column} Column
|
|
||||||
* @typedef {import('typeorm-model-generator').Entity} Entity
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customizes the entity name.
|
|
||||||
* @param {string} oldEntityName - The default entity name.
|
|
||||||
* @param {Entity} entity - The entity.
|
|
||||||
* @returns {string} The new entity name.
|
|
||||||
*/
|
|
||||||
function entityName(oldEntityName, entity) {
|
|
||||||
return oldEntityName + 'Entity';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customizes the column name.
|
|
||||||
* @param {string} oldColumnName - The default column name.
|
|
||||||
* @param {Column} column - The column.
|
|
||||||
* @returns {string} The new column name.
|
|
||||||
*/
|
|
||||||
function columnName(oldColumnName, column) {
|
|
||||||
return oldColumnName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Customizes the file name.
|
|
||||||
* @param {string} oldFileName - The default file name.
|
|
||||||
* @returns {string} The new file name.
|
|
||||||
*/
|
|
||||||
function fileName(oldFileName) {
|
|
||||||
return oldFileName.replace('Entity', '.entity');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
entityName,
|
|
||||||
columnName,
|
|
||||||
fileName,
|
|
||||||
};
|
|
@ -1,16 +1,5 @@
|
|||||||
import baseConfig from "../../eslint.config.mjs";
|
import baseConfig from "../../eslint.config.mjs";
|
||||||
import tseslint from 'typescript-eslint';
|
|
||||||
|
|
||||||
export default tseslint.config([
|
export default [
|
||||||
...baseConfig,
|
...baseConfig
|
||||||
{
|
];
|
||||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
|
||||||
extends: [tseslint.configs.recommendedTypeCheckedOnly],
|
|
||||||
languageOptions: {
|
|
||||||
parserOptions: {
|
|
||||||
projectService: true,
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { Public } from './modules/auth/strategies/jwt/jwt-auth.guard';
|
|
||||||
|
|
||||||
@Public()
|
@Controller()
|
||||||
@Controller({ version: VERSION_NEUTRAL })
|
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
async getData() {
|
getData() {
|
||||||
return await this.appService.getData();
|
return this.appService.getData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,28 +2,8 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
import { AppAuthModule } from './modules/auth/auth.module';
|
|
||||||
import { AppConfigModule } from './modules/config/config.module';
|
|
||||||
import { AppDatabaseModule } from './modules/database/database.module';
|
|
||||||
import { EnumifyModule } from './modules/enumify/enumify.module';
|
|
||||||
import { AppFileTransactionsModule } from './modules/file-transactions/file-transactions.module';
|
|
||||||
import { AppHealthModule } from './modules/health/health.module';
|
|
||||||
import { AppLoggerModule } from './modules/logger/logger.module';
|
|
||||||
import { AppScheduleModule } from './modules/schedule/schedule.module';
|
|
||||||
import { AppValidationModule } from './modules/validation/validation.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [],
|
||||||
AppConfigModule,
|
|
||||||
AppLoggerModule,
|
|
||||||
AppDatabaseModule,
|
|
||||||
AppHealthModule,
|
|
||||||
AppFileTransactionsModule,
|
|
||||||
AppScheduleModule,
|
|
||||||
AppValidationModule,
|
|
||||||
AppAuthModule,
|
|
||||||
EnumifyModule,
|
|
||||||
],
|
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
private readonly logger = new Logger(AppService.name);
|
getData(): { message: string } {
|
||||||
|
|
||||||
async getData() {
|
|
||||||
return ({ message: 'Hello API' });
|
return ({ message: 'Hello API' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { registerAs } from '@nestjs/config';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import coerceRecordTypes from '../config/utils/coerce-record-types';
|
|
||||||
|
|
||||||
export const authSchema = z.object({
|
|
||||||
accessToken: z.object({
|
|
||||||
secret: z.string(),
|
|
||||||
expTimeInSecs: z.number().finite().nonnegative(),
|
|
||||||
}),
|
|
||||||
refreshToken: z.object({
|
|
||||||
secret: z.string(),
|
|
||||||
expTimeInSecs: z.number().finite().nonnegative(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
export type AuthConfig = z.infer<typeof authSchema>;
|
|
||||||
|
|
||||||
export const authConfig = registerAs('auth', () => {
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
|
|
||||||
const config: AuthConfig = authSchema.strict().parse({
|
|
||||||
accessToken: {
|
|
||||||
secret: env['JWT_ACCESS_TOKEN_SECRET'],
|
|
||||||
expTimeInSecs: env['JWT_ACCESS_TOKEN_EXPIRATION_TIME_IN_SECONDS'],
|
|
||||||
},
|
|
||||||
refreshToken: {
|
|
||||||
secret: env['JWT_REFRESH_TOKEN_SECRET'],
|
|
||||||
expTimeInSecs: env['JWT_REFRESH_TOKEN_EXPIRATION_TIME_IN_SECONDS'],
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
});
|
|
@ -1,37 +0,0 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post, UnauthorizedException, UseGuards } from '@nestjs/common';
|
|
||||||
import { ApiBearerAuth } from '@nestjs/swagger';
|
|
||||||
import { AccountsEntity } from '../database/connections/ebitemp-api/entities';
|
|
||||||
import { LoginDto, LoginResDto } from './auth.dto';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { AuthenticatedUser } from './authenticated-user.decorator';
|
|
||||||
import { Public } from './strategies/jwt/jwt-auth.guard';
|
|
||||||
|
|
||||||
@ApiBearerAuth()
|
|
||||||
@Controller('auth')
|
|
||||||
export class AuthController {
|
|
||||||
constructor(private readonly authService: AuthService) {}
|
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Public()
|
|
||||||
@Post('login')
|
|
||||||
async logIn(@Body() body: LoginDto): Promise<LoginResDto> {
|
|
||||||
const { username, password } = body;
|
|
||||||
const user = await this.authService.getAuthenticatedUser(username, password);
|
|
||||||
|
|
||||||
const { accessToken, refreshToken } = await this.authService.signJwts(user);
|
|
||||||
return {
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@Post('refresh')
|
|
||||||
async refresh(@AuthenticatedUser() user: AccountsEntity): Promise<LoginResDto> {
|
|
||||||
const { accessToken, refreshToken } = await this.authService.signJwts(user);
|
|
||||||
return {
|
|
||||||
accessToken: accessToken,
|
|
||||||
refreshToken: refreshToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { createZodDto } from '@anatine/zod-nestjs';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { AccountsEntitySchema } from '../database/connections/ebitemp-api/entities';
|
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
|
||||||
username: AccountsEntitySchema.shape.username,
|
|
||||||
password: z.string().nonempty(),
|
|
||||||
});
|
|
||||||
export type Login = z.infer<typeof loginSchema>;
|
|
||||||
export class LoginDto extends createZodDto(loginSchema) {}
|
|
||||||
|
|
||||||
export const loginResSchema = z.object({
|
|
||||||
accessToken: z.string().jwt(),
|
|
||||||
refreshToken: z.string().jwt(),
|
|
||||||
})
|
|
||||||
export type LoginRes = z.infer<typeof loginResSchema>;
|
|
||||||
export class LoginResDto extends createZodDto(loginResSchema) {}
|
|
@ -1,35 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { APP_GUARD, Reflector } from '@nestjs/core';
|
|
||||||
import { PassportModule } from '@nestjs/passport';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { AccountsEntity } from '../database/connections/ebitemp-api/entities';
|
|
||||||
import { AuthController } from './auth.controller';
|
|
||||||
import { AuthService } from './auth.service';
|
|
||||||
import { JwtAccessTokenAuthStrategy } from './strategies/jwt/jwt-access-token-auth.strategy';
|
|
||||||
import { JwtAccessTokenModule } from './strategies/jwt/jwt-access-token.module';
|
|
||||||
import { JwtAuthGuard } from './strategies/jwt/jwt-auth.guard';
|
|
||||||
import { JwtRefreshTokenAuthStrategy } from './strategies/jwt/jwt-refresh-token-auth.strategy';
|
|
||||||
import { JwtRefreshTokenModule } from './strategies/jwt/jwt-refresh-token.module';
|
|
||||||
import { UsersAuthModule } from './users/users.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
TypeOrmModule.forFeature([AccountsEntity]),
|
|
||||||
JwtAccessTokenModule,
|
|
||||||
JwtRefreshTokenModule,
|
|
||||||
PassportModule,
|
|
||||||
UsersAuthModule,
|
|
||||||
],
|
|
||||||
controllers: [AuthController],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: APP_GUARD,
|
|
||||||
useFactory: (reflector) => new JwtAuthGuard(reflector),
|
|
||||||
inject: [Reflector],
|
|
||||||
},
|
|
||||||
AuthService,
|
|
||||||
JwtAccessTokenAuthStrategy,
|
|
||||||
JwtRefreshTokenAuthStrategy,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppAuthModule {}
|
|
@ -1,49 +0,0 @@
|
|||||||
import { Inject, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
import * as bcrypt from 'bcrypt';
|
|
||||||
import { isEmpty } from 'lodash';
|
|
||||||
import { AccountsEntity } from '../database/connections/ebitemp-api/entities';
|
|
||||||
import { ACCESS_TOKEN_JWT_SERVICE } from './strategies/jwt/jwt-access-token.module';
|
|
||||||
import { REFRESH_TOKEN_JWT_SERVICE } from './strategies/jwt/jwt-refresh-token.module';
|
|
||||||
import { UsersAuthService } from './users/users-auth.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class AuthService {
|
|
||||||
private readonly logger = new Logger(AuthService.name);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly usersAuthService: UsersAuthService,
|
|
||||||
@Inject(ACCESS_TOKEN_JWT_SERVICE) private readonly accessTokenJwtService: JwtService,
|
|
||||||
@Inject(REFRESH_TOKEN_JWT_SERVICE) private readonly refreshTokenJwtService: JwtService
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async signJwts(user: AccountsEntity) {
|
|
||||||
const payload = { username: user.username, sub: user.id };
|
|
||||||
|
|
||||||
const accessToken = await this.accessTokenJwtService.signAsync(payload);
|
|
||||||
|
|
||||||
const refreshToken = await this.refreshTokenJwtService.signAsync(payload);
|
|
||||||
await this.usersAuthService.setCurrentRefreshTokenHash(user.id, refreshToken);
|
|
||||||
|
|
||||||
return { accessToken, refreshToken };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAuthenticatedUser(username: string, password: string): Promise<AccountsEntity> {
|
|
||||||
try {
|
|
||||||
const account = await this.usersAuthService.getUserByUsername(username);
|
|
||||||
if (!account) throw new UnauthorizedException(`Username not found`);
|
|
||||||
await this.verifyPassword(password, account.password);
|
|
||||||
return account;
|
|
||||||
} catch (error) {
|
|
||||||
throw new UnauthorizedException(`Wrong credentials`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async verifyPassword(plainTextPassword: string, hashedPassword: string | null) {
|
|
||||||
const isPasswordMatching =
|
|
||||||
hashedPassword && !isEmpty(hashedPassword) ? await bcrypt.compare(plainTextPassword, hashedPassword) : null;
|
|
||||||
if (!isPasswordMatching) {
|
|
||||||
throw new UnauthorizedException(`Wrong password`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
|
||||||
import { RequestWithUser } from './constants/request-with-user';
|
|
||||||
|
|
||||||
export const AuthenticatedUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
|
|
||||||
const request = ctx.switchToHttp().getRequest<RequestWithUser>();
|
|
||||||
|
|
||||||
return request.user;
|
|
||||||
});
|
|
@ -1,6 +0,0 @@
|
|||||||
import { FastifyRequest } from "fastify";
|
|
||||||
import { AccountsEntity } from "../../database/connections/ebitemp-api/entities";
|
|
||||||
|
|
||||||
export interface RequestWithUser extends FastifyRequest {
|
|
||||||
user: AccountsEntity;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface TokenPayload {
|
|
||||||
sub: number;
|
|
||||||
iat?: number;
|
|
||||||
exp?: number;
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
||||||
import { AuthConfig, authConfig } from '../../auth.config';
|
|
||||||
import { TokenPayload } from '../../constants/token-payload.interface';
|
|
||||||
import { UsersAuthService } from '../../users/users-auth.service';
|
|
||||||
import { FastifyRequest } from 'fastify';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JwtAccessTokenAuthStrategy extends PassportStrategy(Strategy, 'jwt-access-token') {
|
|
||||||
constructor(@Inject(authConfig.KEY) authConfig: AuthConfig, private readonly userAuthService: UsersAuthService) {
|
|
||||||
super({
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
secretOrKey: authConfig.accessToken.secret,
|
|
||||||
passReqToCallback: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(request: FastifyRequest, payload: TokenPayload) {
|
|
||||||
const account = await this.userAuthService.getUserById(payload.sub);
|
|
||||||
if (!account) throw new UnauthorizedException('Access Token Guard');
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { JwtService, JwtModule as NestJwtModule } from '@nestjs/jwt';
|
|
||||||
import { authConfig, AuthConfig } from '../../auth.config';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
|
|
||||||
export const ACCESS_TOKEN_JWT_SERVICE = Symbol('ACCESS_TOKEN_JWT_SERVICE');
|
|
||||||
const accessTokenJwtProvider = {
|
|
||||||
provide: ACCESS_TOKEN_JWT_SERVICE,
|
|
||||||
useFactory: (jwtService: JwtService) => jwtService,
|
|
||||||
inject: [JwtService],
|
|
||||||
};
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forFeature(authConfig),
|
|
||||||
NestJwtModule.registerAsync({
|
|
||||||
imports: [...authConfig.asProvider().imports],
|
|
||||||
useFactory: async (authConfig: AuthConfig) => ({
|
|
||||||
secret: authConfig.accessToken.secret,
|
|
||||||
signOptions: {
|
|
||||||
expiresIn: `${authConfig.accessToken.expTimeInSecs}s`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
inject: [authConfig.KEY],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [accessTokenJwtProvider],
|
|
||||||
exports: [ConfigModule, accessTokenJwtProvider],
|
|
||||||
})
|
|
||||||
export class JwtAccessTokenModule {}
|
|
@ -1,36 +0,0 @@
|
|||||||
import { applyDecorators, ExecutionContext, SetMetadata } from '@nestjs/common';
|
|
||||||
import { Reflector } from '@nestjs/core';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
|
||||||
import { OpenAPIObject } from '@nestjs/swagger';
|
|
||||||
|
|
||||||
export const IS_PUBLIC_KEY = 'isPublic';
|
|
||||||
export const Public = () =>
|
|
||||||
applyDecorators(SetMetadata(IS_PUBLIC_KEY, true), SetMetadata('swagger/apiSecurity', ['public']));
|
|
||||||
|
|
||||||
export const patchPublicDecoratorSupport = (document: OpenAPIObject) => {
|
|
||||||
Object.values(document.paths).forEach((path: any) => {
|
|
||||||
Object.values(path).forEach((method: any) => {
|
|
||||||
if (Array.isArray(method.security) && method.security.includes('public')) {
|
|
||||||
method.security = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return document;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class JwtAuthGuard extends AuthGuard(['jwt-access-token', 'jwt-refresh-token']) {
|
|
||||||
constructor(private reflector: Reflector) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
canActivate(context: ExecutionContext) {
|
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
||||||
context.getHandler(),
|
|
||||||
context.getClass(),
|
|
||||||
]);
|
|
||||||
if (isPublic) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.canActivate(context);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
|
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
|
||||||
import { FastifyRequest } from 'fastify';
|
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
||||||
import { authConfig, AuthConfig } from '../../auth.config';
|
|
||||||
import { TokenPayload } from '../../constants/token-payload.interface';
|
|
||||||
import { UsersAuthService } from '../../users/users-auth.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class JwtRefreshTokenAuthStrategy extends PassportStrategy(Strategy, 'jwt-refresh-token') {
|
|
||||||
constructor(@Inject(authConfig.KEY) authConfig: AuthConfig, private readonly usersAuthService: UsersAuthService) {
|
|
||||||
super({
|
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
||||||
secretOrKey: authConfig.refreshToken.secret,
|
|
||||||
passReqToCallback: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async validate(request: FastifyRequest, payload: TokenPayload) {
|
|
||||||
const refreshToken = (request.headers?.authorization as string| undefined)?.replace('Bearer', '')?.trim() ?? '';
|
|
||||||
const account = await this.usersAuthService.getUserByIdAndRefreshTokenPair(payload.sub, refreshToken);
|
|
||||||
if (!account) throw new UnauthorizedException('Refresh Token Guard');
|
|
||||||
return account;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { JwtService, JwtModule as NestJwtModule } from '@nestjs/jwt';
|
|
||||||
import { authConfig, AuthConfig } from '../../auth.config';
|
|
||||||
|
|
||||||
export const REFRESH_TOKEN_JWT_SERVICE = Symbol('REFRESH_TOKEN_JWT_SERVICE');
|
|
||||||
const refreshTokenJwtProvider = {
|
|
||||||
provide: REFRESH_TOKEN_JWT_SERVICE,
|
|
||||||
useFactory: (jwtService: JwtService) => jwtService,
|
|
||||||
inject: [JwtService],
|
|
||||||
};
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forFeature(authConfig),
|
|
||||||
NestJwtModule.registerAsync({
|
|
||||||
imports: [...authConfig.asProvider().imports],
|
|
||||||
useFactory: async (authConfig: AuthConfig) => ({
|
|
||||||
secret: authConfig.refreshToken.secret,
|
|
||||||
signOptions: {
|
|
||||||
expiresIn: `${authConfig.refreshToken.expTimeInSecs}s`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
inject: [authConfig.KEY],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [refreshTokenJwtProvider],
|
|
||||||
exports: [ConfigModule, refreshTokenJwtProvider],
|
|
||||||
})
|
|
||||||
export class JwtRefreshTokenModule {}
|
|
@ -1,42 +0,0 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import bcrypt from 'bcrypt';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { AccountsEntity } from '../../database/connections/ebitemp-api/entities';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class UsersAuthService {
|
|
||||||
constructor(
|
|
||||||
@InjectRepository(AccountsEntity)
|
|
||||||
private readonly accountsRepository: Repository<AccountsEntity>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async getUserById(accountId: number) {
|
|
||||||
return await this.accountsRepository.findOne({
|
|
||||||
relations: { profili: { ruolo: true } },
|
|
||||||
where: { id: accountId },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByUsername(username: string) {
|
|
||||||
return await this.accountsRepository.findOne({
|
|
||||||
relations: { profili: { ruolo: true } },
|
|
||||||
where: { username: username },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserByIdAndRefreshTokenPair(accountId: number, refreshToken: string) {
|
|
||||||
const accountById = await this.getUserById(accountId);
|
|
||||||
if (!accountById?.ultimoHashRefreshToken) return null;
|
|
||||||
|
|
||||||
const isRefreshTokenMatching = await bcrypt.compare(refreshToken, accountById.ultimoHashRefreshToken);
|
|
||||||
return isRefreshTokenMatching ? accountById : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setCurrentRefreshTokenHash(accountId: number, refreshToken: string | null) {
|
|
||||||
const hash = refreshToken ? await bcrypt.hash(refreshToken, 10) : null;
|
|
||||||
await this.accountsRepository.update(accountId, {
|
|
||||||
ultimoHashRefreshToken: hash,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { UsersAuthService } from './users-auth.service';
|
|
||||||
import { AccountsEntity } from '../../database/connections/ebitemp-api/entities';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([AccountsEntity])],
|
|
||||||
controllers: [],
|
|
||||||
providers: [UsersAuthService],
|
|
||||||
exports: [UsersAuthService],
|
|
||||||
})
|
|
||||||
export class UsersAuthModule {}
|
|
@ -1,13 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { localConfig } from './local.config';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forRoot({
|
|
||||||
isGlobal: true,
|
|
||||||
load: [localConfig],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppConfigModule {}
|
|
@ -1,19 +0,0 @@
|
|||||||
import coerceRecordTypes from './utils/coerce-record-types';
|
|
||||||
import { registerAs } from '@nestjs/config';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const localSchema = z.object({
|
|
||||||
production: z.boolean(),
|
|
||||||
port: z.number().optional(),
|
|
||||||
});
|
|
||||||
export type LocalConfig = z.infer<typeof localSchema>;
|
|
||||||
|
|
||||||
export const localConfig = registerAs('local', () => {
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
|
|
||||||
const config: LocalConfig = localSchema.strict().parse({
|
|
||||||
production: env['PRODUCTION'],
|
|
||||||
port: env['PORT'],
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
import { clone, each } from 'lodash';
|
|
||||||
|
|
||||||
export default (raw: Record<string, any>): Record<string, string | number | boolean | undefined> => {
|
|
||||||
raw = clone(raw);
|
|
||||||
return each(raw, (value, key) => {
|
|
||||||
if (value === 'true') raw[key] = true;
|
|
||||||
if (value === 'false') raw[key] = false;
|
|
||||||
if (value === 'null') raw[key] = null;
|
|
||||||
if (!isNaN(Number(value))) raw[key] = Number(value);
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import coerceRecordTypes from '../../../config/utils/coerce-record-types';
|
|
||||||
import { databaseConfigFactory, rawDatabaseSchema } from '../../utils/database-config';
|
|
||||||
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
const envParsed = rawDatabaseSchema.strict().parse({
|
|
||||||
connectionString: env['DATABASE_EBITEMPAPI_CONNECTION_STRING'],
|
|
||||||
secure: env['DATABASE_EBITEMPAPI_SECURE'],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const databaseConfig = databaseConfigFactory(envParsed);
|
|
@ -1,29 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { DatabaseConfig } from '../../utils/database-config';
|
|
||||||
import { typeormTransactionalDataSourceFactory } from '../../utils/typeorm-data-source-factory';
|
|
||||||
import { typeormEntitiesFromImport } from '../../utils/typeorm-import-entities';
|
|
||||||
import { typeormModuleOptionsFactory } from '../../utils/typeorm-module-options-factory';
|
|
||||||
import { databaseConfig } from './database.config';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forFeature(databaseConfig),
|
|
||||||
TypeOrmModule.forRootAsync({
|
|
||||||
imports: databaseConfig.asProvider().imports,
|
|
||||||
dataSourceFactory: typeormTransactionalDataSourceFactory(),
|
|
||||||
useFactory: async (dbConfig: DatabaseConfig) => {
|
|
||||||
const config = await typeormModuleOptionsFactory(
|
|
||||||
dbConfig,
|
|
||||||
await typeormEntitiesFromImport(await import('./index'))
|
|
||||||
);
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
inject: [databaseConfig.KEY],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [],
|
|
||||||
exports: [TypeOrmModule],
|
|
||||||
})
|
|
||||||
export class EbitempApiDatabaseModule {}
|
|
@ -1,51 +0,0 @@
|
|||||||
import { toZod } from 'tozod';
|
|
||||||
import { Column, Entity, Index, OneToMany } from 'typeorm';
|
|
||||||
import { z } from 'zod';
|
|
||||||
import { ProfiliEntity, ProfiliEntitySchema } from './profili.entity';
|
|
||||||
|
|
||||||
@Index('pk_accounts', ['id'], { unique: true })
|
|
||||||
@Entity('accounts')
|
|
||||||
export class AccountsEntity {
|
|
||||||
@Column('int', { primary: true, name: 'id' })
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'username', unique: true, length: 255 })
|
|
||||||
username!: string;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'password', nullable: true, length: 255 })
|
|
||||||
password!: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'nome', length: 255 })
|
|
||||||
nome!: string;
|
|
||||||
|
|
||||||
@Column('date', { name: 'data_creazione' })
|
|
||||||
dataCreazione!: Date;
|
|
||||||
|
|
||||||
@Column('date', { name: 'data_scadenza', nullable: true })
|
|
||||||
dataScadenza!: Date | null;
|
|
||||||
|
|
||||||
@Column('varchar', {
|
|
||||||
name: 'ultimo_hash_refresh_token',
|
|
||||||
nullable: true,
|
|
||||||
length: 1024,
|
|
||||||
})
|
|
||||||
ultimoHashRefreshToken!: string | null;
|
|
||||||
|
|
||||||
@OneToMany(() => ProfiliEntity, (profiliEntity) => profiliEntity.account)
|
|
||||||
profili!: ProfiliEntity[];
|
|
||||||
|
|
||||||
constructor(init?: Partial<AccountsEntity>) {
|
|
||||||
Object.assign(this, init);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AccountsEntitySchema: toZod<AccountsEntity> = z.late.object(() => ({
|
|
||||||
id: z.number().finite(),
|
|
||||||
username: z.string().nonempty(),
|
|
||||||
password: z.string().nonempty().nullable(),
|
|
||||||
nome: z.string().nonempty(),
|
|
||||||
dataCreazione: z.date(),
|
|
||||||
dataScadenza: z.date().nullable(),
|
|
||||||
ultimoHashRefreshToken: z.string().nonempty().nullable(),
|
|
||||||
profili: z.array(ProfiliEntitySchema),
|
|
||||||
}));
|
|
@ -1,4 +0,0 @@
|
|||||||
export * from './accounts.entity';
|
|
||||||
export * from './profili.entity';
|
|
||||||
export * from './ruoli.entity';
|
|
||||||
export * from './tipi_jobs.entity';
|
|
@ -1,38 +0,0 @@
|
|||||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
|
||||||
import { AccountsEntity, AccountsEntitySchema } from './accounts.entity';
|
|
||||||
import { RuoliEntity, RuoliEntitySchema } from './ruoli.entity';
|
|
||||||
import { toZod } from 'tozod';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
@Index('pk_profili', ['id'], { unique: true })
|
|
||||||
@Entity('profili')
|
|
||||||
export class ProfiliEntity {
|
|
||||||
@Column('int', { primary: true, name: 'id' })
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column('int', { name: 'id_account', unique: true })
|
|
||||||
idAccount!: number;
|
|
||||||
|
|
||||||
@Column('int', { name: 'id_ruolo', unique: true })
|
|
||||||
idRuolo!: number;
|
|
||||||
|
|
||||||
@ManyToOne(() => AccountsEntity, (accountsEntity) => accountsEntity.profili)
|
|
||||||
@JoinColumn([{ name: 'id_account', referencedColumnName: 'id' }])
|
|
||||||
account!: AccountsEntity;
|
|
||||||
|
|
||||||
@ManyToOne(() => RuoliEntity, (ruoliEntity) => ruoliEntity.profili)
|
|
||||||
@JoinColumn([{ name: 'id_ruolo', referencedColumnName: 'id' }])
|
|
||||||
ruolo!: RuoliEntity;
|
|
||||||
|
|
||||||
constructor(init?: Partial<ProfiliEntity>) {
|
|
||||||
Object.assign(this, init);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProfiliEntitySchema: toZod<ProfiliEntity> = z.late.object(() => ({
|
|
||||||
id: z.number().finite(),
|
|
||||||
idAccount: AccountsEntitySchema.shape.id,
|
|
||||||
idRuolo: RuoliEntitySchema.shape.id,
|
|
||||||
account: AccountsEntitySchema,
|
|
||||||
ruolo: RuoliEntitySchema,
|
|
||||||
}));
|
|
@ -1,31 +0,0 @@
|
|||||||
import { Column, Entity, Index, OneToMany } from 'typeorm';
|
|
||||||
import { ProfiliEntity, ProfiliEntitySchema } from './profili.entity';
|
|
||||||
import { toZod } from 'tozod';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
@Index('pk_ruoli', ['id'], { unique: true })
|
|
||||||
@Entity('ruoli')
|
|
||||||
export class RuoliEntity {
|
|
||||||
@Column('int', { primary: true, name: 'id' })
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'nome', unique: true, length: 255 })
|
|
||||||
nome!: string;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'descrizione', length: 255 })
|
|
||||||
descrizione!: string;
|
|
||||||
|
|
||||||
@OneToMany(() => ProfiliEntity, (profiliEntity) => profiliEntity.ruolo)
|
|
||||||
profili!: ProfiliEntity[];
|
|
||||||
|
|
||||||
constructor(init?: Partial<RuoliEntity>) {
|
|
||||||
Object.assign(this, init);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RuoliEntitySchema: toZod<RuoliEntity> = z.late.object(() => ({
|
|
||||||
id: z.number().finite(),
|
|
||||||
nome: z.string().nonempty(),
|
|
||||||
descrizione: z.string().nonempty(),
|
|
||||||
profili: z.array(ProfiliEntitySchema),
|
|
||||||
}));
|
|
@ -1,49 +0,0 @@
|
|||||||
import { Column, Entity, Index } from 'typeorm';
|
|
||||||
import { Enumify } from '../../../../enumify/enumify';
|
|
||||||
|
|
||||||
export class InviaMailSeErrori {
|
|
||||||
@Column('bit', { name: 'flag', default: () => '(0)' })
|
|
||||||
flagAttivo!: boolean;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'oggetto', nullable: true, length: 255 })
|
|
||||||
oggetto!: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'destinatari', nullable: true, length: 255 })
|
|
||||||
destinatari!: string | null;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'cc', nullable: true, length: 255 })
|
|
||||||
cc!: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Index('pk_tipi_jobs', ['id'], { unique: true })
|
|
||||||
@Entity('tipi_jobs')
|
|
||||||
export class TipiJobsEntity extends Enumify {
|
|
||||||
static _ = TipiJobsEntity.closeEnum();
|
|
||||||
|
|
||||||
@Column('int', { primary: true, name: 'id' })
|
|
||||||
id!: number;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'nome', length: 50 })
|
|
||||||
nome!: string;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'descrizione', nullable: true, length: 255 })
|
|
||||||
descrizione!: string | null;
|
|
||||||
|
|
||||||
@Column(() => InviaMailSeErrori, { prefix: 'invia_mail_se_errori'})
|
|
||||||
inviaMailSeErrori!: InviaMailSeErrori;
|
|
||||||
|
|
||||||
@Column('bit', { name: 'flag_attivo', default: () => '(0)' })
|
|
||||||
flagAttivo!: boolean;
|
|
||||||
|
|
||||||
@Column('varchar', { name: 'pattern_cron', length: 20 })
|
|
||||||
patternCron!: string;
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
id: number,
|
|
||||||
nome: string,
|
|
||||||
patternCron: string,
|
|
||||||
) {
|
|
||||||
super(id, nome);
|
|
||||||
this.patternCron = patternCron;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './entities';
|
|
@ -1,10 +0,0 @@
|
|||||||
import coerceRecordTypes from '../../../config/utils/coerce-record-types';
|
|
||||||
import { databaseConfigFactory, rawDatabaseSchema } from '../../utils/database-config';
|
|
||||||
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
const envParsed = rawDatabaseSchema.strict().parse({
|
|
||||||
connectionString: env['DATABASE_OCEANO_CONNECTION_STRING'],
|
|
||||||
secure: env['DATABASE_OCEANO_SECURE'],
|
|
||||||
});
|
|
||||||
|
|
||||||
export const databaseConfig = databaseConfigFactory(envParsed);
|
|
@ -1,29 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { DatabaseConfig } from '../../utils/database-config';
|
|
||||||
import { typeormTransactionalDataSourceFactory } from '../../utils/typeorm-data-source-factory';
|
|
||||||
import { typeormEntitiesFromImport } from '../../utils/typeorm-import-entities';
|
|
||||||
import { typeormModuleOptionsFactory } from '../../utils/typeorm-module-options-factory';
|
|
||||||
import { databaseConfig } from './database.config';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forFeature(databaseConfig),
|
|
||||||
TypeOrmModule.forRootAsync({
|
|
||||||
imports: databaseConfig.asProvider().imports,
|
|
||||||
dataSourceFactory: typeormTransactionalDataSourceFactory(),
|
|
||||||
useFactory: async (dbConfig: DatabaseConfig) => {
|
|
||||||
const config = await typeormModuleOptionsFactory(
|
|
||||||
dbConfig,
|
|
||||||
await typeormEntitiesFromImport(await import('./index'))
|
|
||||||
);
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
inject: [databaseConfig.KEY],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [],
|
|
||||||
exports: [TypeOrmModule],
|
|
||||||
})
|
|
||||||
export class OceanoDatabaseModule {}
|
|
@ -1,653 +0,0 @@
|
|||||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
|
||||||
@Index('PK_gruppo', ['codiceGruppo'], { unique: true })
|
|
||||||
@Entity('gruppo')
|
|
||||||
export class GruppoEntity {
|
|
||||||
@Column('bigint', { primary: true, name: 'codiceGruppo' })
|
|
||||||
codiceGruppo!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'gruppo', nullable: true, length: 80 })
|
|
||||||
gruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'descrizioneGruppo',
|
|
||||||
nullable: true,
|
|
||||||
length: 255,
|
|
||||||
})
|
|
||||||
descrizioneGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'noteGruppo', nullable: true })
|
|
||||||
noteGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'referenteGruppo', nullable: true, length: 80 })
|
|
||||||
referenteGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'telefonoReferente', nullable: true, length: 80 })
|
|
||||||
telefonoReferente!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'cellulareReferente',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
cellulareReferente!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'emailReferente', nullable: true, length: 80 })
|
|
||||||
emailReferente!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'indirizzoGruppo', nullable: true, length: 80 })
|
|
||||||
indirizzoGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'numeroCivicoGruppo',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
numeroCivicoGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'capGruppo', nullable: true, length: 10 })
|
|
||||||
capGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'localitaGruppo', nullable: true, length: 80 })
|
|
||||||
localitaGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'telefono1Gruppo', nullable: true, length: 80 })
|
|
||||||
telefono1Gruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'telefono2Gruppo', nullable: true, length: 80 })
|
|
||||||
telefono2Gruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'faxGruppo', nullable: true, length: 80 })
|
|
||||||
faxGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'emailGruppo', nullable: true, length: 80 })
|
|
||||||
emailGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'sitoWebGruppo', nullable: true, length: 80 })
|
|
||||||
sitoWebGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('int', { name: 'gruppoAttivo', nullable: true })
|
|
||||||
gruppoAttivo!: number | null;
|
|
||||||
|
|
||||||
@Column('int', { name: 'esolverAttivo', nullable: true })
|
|
||||||
esolverAttivo!: number | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'serverEsolver', nullable: true, length: 80 })
|
|
||||||
serverEsolver!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'databaseEsolver', nullable: true, length: 80 })
|
|
||||||
databaseEsolver!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'usernameEsolver', nullable: true, length: 80 })
|
|
||||||
usernameEsolver!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'passwordEsolver', nullable: true, length: 80 })
|
|
||||||
passwordEsolver!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'gruppoEsolver', nullable: true, length: 80 })
|
|
||||||
gruppoEsolver!: string | null;
|
|
||||||
|
|
||||||
@Column('image', { name: 'logoGruppo', nullable: true })
|
|
||||||
logoGruppo!: Buffer | null;
|
|
||||||
|
|
||||||
@Column('bigint', { name: 'utenteCreazione', nullable: true })
|
|
||||||
utenteCreazione!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'dataCreazione', nullable: true, length: 10 })
|
|
||||||
dataCreazione!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'oraCreazione', nullable: true, length: 8 })
|
|
||||||
oraCreazione!: string | null;
|
|
||||||
|
|
||||||
@Column('bigint', { name: 'utenteModifica', nullable: true })
|
|
||||||
utenteModifica!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'dataModifica', nullable: true, length: 10 })
|
|
||||||
dataModifica!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'oraModifica', nullable: true, length: 8 })
|
|
||||||
oraModifica!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'direttivaReport', nullable: true, length: 512 })
|
|
||||||
direttivaReport!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'direttivaReportPers',
|
|
||||||
nullable: true,
|
|
||||||
length: 512,
|
|
||||||
})
|
|
||||||
direttivaReportPers!: string | null;
|
|
||||||
|
|
||||||
@Column('bigint', { name: 'statoGruppo', nullable: true })
|
|
||||||
statoGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('bigint', { name: 'provinciaGruppo', nullable: true })
|
|
||||||
provinciaGruppo!: string | null;
|
|
||||||
|
|
||||||
@Column('int', { name: 'fondo', nullable: true })
|
|
||||||
fondo!: number | null;
|
|
||||||
|
|
||||||
@Column('decimal', {
|
|
||||||
name: 'tassaIscrizione',
|
|
||||||
nullable: true,
|
|
||||||
precision: 18,
|
|
||||||
scale: 2,
|
|
||||||
})
|
|
||||||
tassaIscrizione!: number | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'codiceFiscale', nullable: true, length: 80 })
|
|
||||||
codiceFiscale!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'serverFTP', nullable: true, length: 200 })
|
|
||||||
serverFtp!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'usernameFTP', nullable: true, length: 200 })
|
|
||||||
usernameFtp!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'passwordFTP', nullable: true, length: 200 })
|
|
||||||
passwordFtp!: string | null;
|
|
||||||
|
|
||||||
@Column('decimal', {
|
|
||||||
name: 'massimalePresaCarico',
|
|
||||||
nullable: true,
|
|
||||||
precision: 18,
|
|
||||||
scale: 2,
|
|
||||||
})
|
|
||||||
massimalePresaCarico!: number | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'elencoLivello1', nullable: true, length: 80 })
|
|
||||||
elencoLivello1!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'testoEmailPreventivo', nullable: true })
|
|
||||||
testoEmailPreventivo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'emailPreventivoMittente',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
emailPreventivoMittente!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'emailPreventivoNotifica',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
emailPreventivoNotifica!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'desPreventivoMittente',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
desPreventivoMittente!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'desPreventivoNotifica',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
desPreventivoNotifica!: string | null;
|
|
||||||
|
|
||||||
@Column('bigint', { name: 'codiceContrattoIndividuali', nullable: true })
|
|
||||||
codiceContrattoIndividuali!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'codiceAziendaBollettini',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
codiceAziendaBollettini!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'rigaTesserino1', nullable: true, length: 80 })
|
|
||||||
rigaTesserino1!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'rigaTesserino2', nullable: true, length: 80 })
|
|
||||||
rigaTesserino2!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'rigaTesserino3', nullable: true, length: 80 })
|
|
||||||
rigaTesserino3!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'cartellaCondivisa',
|
|
||||||
nullable: true,
|
|
||||||
length: 512,
|
|
||||||
})
|
|
||||||
cartellaCondivisa!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'serverSMTP', nullable: true, length: 80 })
|
|
||||||
serverSmtp!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'usernameSMTP', nullable: true, length: 80 })
|
|
||||||
usernameSmtp!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'passwordSMTP', nullable: true, length: 80 })
|
|
||||||
passwordSmtp!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'emailIBAN', nullable: true, length: 255 })
|
|
||||||
emailIban!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'emailFatture', nullable: true, length: 255 })
|
|
||||||
emailFatture!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'emailRA', nullable: true, length: 255 })
|
|
||||||
emailRa!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'emailAreaRiservataSoci',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
emailAreaRiservataSoci!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'desAreaRiservataSoci',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
desAreaRiservataSoci!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'usernameSMS',
|
|
||||||
nullable: true,
|
|
||||||
length: 255,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
usernameSms!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'passwordSMS',
|
|
||||||
nullable: true,
|
|
||||||
length: 255,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
passwordSms!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittenteSMS',
|
|
||||||
nullable: true,
|
|
||||||
length: 255,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
mittenteSms!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'sitoWebDati', length: 255, default: () => "''" })
|
|
||||||
sitoWebDati!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'identificativoCreditore',
|
|
||||||
length: 35,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
identificativoCreditore!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'SMSProtocollo',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
smsProtocollo!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailBonifico',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailBonifico!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', {
|
|
||||||
name: 'emailBonifico',
|
|
||||||
nullable: true,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
emailBonifico!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', {
|
|
||||||
name: 'emailAssegno',
|
|
||||||
nullable: true,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
emailAssegno!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailAssegno',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailAssegno!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'SMSRimborsoBonifico',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
smsRimborsoBonifico!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'SMSRimborsoAssegno',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
smsRimborsoAssegno!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'emailPIC', nullable: true, length: 255 })
|
|
||||||
emailPic!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'stelline', length: 10, default: () => "''" })
|
|
||||||
stelline!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'mittentePIC', length: 400, default: () => "''" })
|
|
||||||
mittentePic!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'oggettoPIC', default: () => "''" })
|
|
||||||
oggettoPic!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'messaggioPIC', default: () => "''" })
|
|
||||||
messaggioPic!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'nomeSupportoContrattoPosta',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
nomeSupportoContrattoPosta!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'codiceContrattoPosta',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
codiceContrattoPosta!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'passwordContrattoPosta',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
passwordContrattoPosta!: string | null;
|
|
||||||
|
|
||||||
@Column('bigint', {
|
|
||||||
name: 'codiceSpedizione',
|
|
||||||
nullable: true,
|
|
||||||
default: () => '(0)',
|
|
||||||
})
|
|
||||||
codiceSpedizione!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailBonificoDomiciliato',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailBonificoDomiciliato!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', {
|
|
||||||
name: 'emailBonificoDomiciliato',
|
|
||||||
nullable: true,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
emailBonificoDomiciliato!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'SMSRimborsoBonificoDomiciliato',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
smsRimborsoBonificoDomiciliato!: string | null;
|
|
||||||
|
|
||||||
@Column('int', { name: 'notifichePush', default: () => '(0)' })
|
|
||||||
notifichePush!: number;
|
|
||||||
|
|
||||||
@Column('int', { name: 'controlloCodiceFiscale', default: () => '(-1)' })
|
|
||||||
controlloCodiceFiscale!: number;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailSuperamentoPIC', default: () => "''" })
|
|
||||||
emailSuperamentoPic!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'SMSProtocolloNotificaEsplicita',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
smsProtocolloNotificaEsplicita!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittenteSportello',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
mittenteSportello!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailBonificoDomiciliatoSportello',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailBonificoDomiciliatoSportello!: string;
|
|
||||||
|
|
||||||
@Column('ntext', {
|
|
||||||
name: 'emailBonificoDomiciliatoSportello',
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
emailBonificoDomiciliatoSportello!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailBonificoSportello',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailBonificoSportello!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailBonificoSportello', default: () => "''" })
|
|
||||||
emailBonificoSportello!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailSospensioneSportello',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailSospensioneSportello!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailSospensioneSportello', default: () => "''" })
|
|
||||||
emailSospensioneSportello!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailRespingimentoSportello',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailRespingimentoSportello!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailRespingimentoSportello', default: () => "''" })
|
|
||||||
emailRespingimentoSportello!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailSospensione',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailSospensione!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailSospensione', default: () => "''" })
|
|
||||||
emailSospensione!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailRespingimento',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailRespingimento!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailRespingimento', default: () => "''" })
|
|
||||||
emailRespingimento!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentePICdisdetta',
|
|
||||||
nullable: true,
|
|
||||||
length: 400,
|
|
||||||
})
|
|
||||||
mittentePiCdisdetta!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'oggettoPICdisdetta', nullable: true })
|
|
||||||
oggettoPiCdisdetta!: string | null;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'messaggioPICdisdetta', nullable: true })
|
|
||||||
messaggioPiCdisdetta!: string | null;
|
|
||||||
|
|
||||||
@Column('int', { name: 'modalitaMassimaleBonifico', default: () => '(0)' })
|
|
||||||
modalitaMassimaleBonifico!: number;
|
|
||||||
|
|
||||||
@Column('nvarchar', { name: 'portaSMTP', length: 10, default: () => "''" })
|
|
||||||
portaSmtp!: string;
|
|
||||||
|
|
||||||
@Column('int', { name: 'useSSL', default: () => '(0)' })
|
|
||||||
useSsl!: number;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'serverSMTPSpot',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
serverSmtpSpot!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'usernameSMTPSpot',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
usernameSmtpSpot!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'passwordSMTPSpot',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
passwordSmtpSpot!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'portaSMTPSpot',
|
|
||||||
length: 10,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
portaSmtpSpot!: string;
|
|
||||||
|
|
||||||
@Column('int', { name: 'useSSLSpot', default: () => '(0)' })
|
|
||||||
useSslSpot!: number;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'cartellaPubblicazioneLocale',
|
|
||||||
length: 512,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
cartellaPubblicazioneLocale!: string;
|
|
||||||
|
|
||||||
@Column('int', { name: 'portaFTP', default: () => '(0)' })
|
|
||||||
portaFtp!: number;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'cartellaAllegatiFTP',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
cartellaAllegatiFtp!: string;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailNotificaEmissioneRimborsoSocio',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailNotificaEmissioneRimborsoSocio!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailInvioAllegatiPraticaSocio',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailInvioAllegatiPraticaSocio!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailRichiestaDatiBancariSocio',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailRichiestaDatiBancariSocio!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailLettereRichiestaContributiRinnoviIndividuali',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailLettereRichiestaContributiRinnoviIndividuali!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailLettereSoci',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailLettereSoci!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailLettereStrutture',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailLettereStrutture!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'mittentiEmailLettereContratti',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
mittentiEmailLettereContratti!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'emailNotificaCodiciFiscaliDoppiMittente',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
emailNotificaCodiciFiscaliDoppiMittente!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'emailNotificaCodiciFiscaliDoppiDestinatari',
|
|
||||||
nullable: true,
|
|
||||||
length: 80,
|
|
||||||
})
|
|
||||||
emailNotificaCodiciFiscaliDoppiDestinatari!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'emailNotificaCodiciFiscaliDoppiCc',
|
|
||||||
nullable: true,
|
|
||||||
length: 200,
|
|
||||||
})
|
|
||||||
emailNotificaCodiciFiscaliDoppiCc!: string | null;
|
|
||||||
|
|
||||||
@Column('nvarchar', {
|
|
||||||
name: 'oggettoEmailRichiestaDatiBancari',
|
|
||||||
length: 200,
|
|
||||||
default: () => "''",
|
|
||||||
})
|
|
||||||
oggettoEmailRichiestaDatiBancari!: string;
|
|
||||||
|
|
||||||
@Column('ntext', { name: 'emailRichiestaDatiBancari', default: () => "''" })
|
|
||||||
emailRichiestaDatiBancari!: string;
|
|
||||||
|
|
||||||
@Column('int', {
|
|
||||||
name: 'flagAllegaModuloIbanEmailRichiestaDatiBancari',
|
|
||||||
nullable: true,
|
|
||||||
})
|
|
||||||
flagAllegaModuloIbanEmailRichiestaDatiBancari!: number | null;
|
|
||||||
|
|
||||||
constructor(init?: Partial<GruppoEntity>) {
|
|
||||||
Object.assign(this, init);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './gruppo.entity';
|
|
@ -1 +0,0 @@
|
|||||||
export * from './entities';
|
|
@ -1 +0,0 @@
|
|||||||
export const APP_DATASOURCES = Symbol('APP_DATASOURCES');
|
|
@ -1,19 +0,0 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
|
||||||
import { APP_DATASOURCES } from './database.constants';
|
|
||||||
import { EbitempApiDatabaseModule } from './connections/ebitemp-api/database.module';
|
|
||||||
import { dataSources } from './utils/typeorm-data-source-factory';
|
|
||||||
|
|
||||||
const dataSourcesProvider = {
|
|
||||||
provide: APP_DATASOURCES,
|
|
||||||
useValue: dataSources,
|
|
||||||
};
|
|
||||||
|
|
||||||
const databaseModules = [EbitempApiDatabaseModule];
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
imports: [...databaseModules],
|
|
||||||
providers: [dataSourcesProvider],
|
|
||||||
exports: [dataSourcesProvider, ...databaseModules],
|
|
||||||
})
|
|
||||||
export class AppDatabaseModule {}
|
|
@ -1,57 +0,0 @@
|
|||||||
import { registerAs } from '@nestjs/config';
|
|
||||||
import { ConnectionString } from 'connection-string';
|
|
||||||
import { first } from 'lodash';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const databaseSchema = z.object({
|
|
||||||
connectionString: z.string(),
|
|
||||||
type: z.enum([
|
|
||||||
'mysql',
|
|
||||||
'postgres',
|
|
||||||
'cockroachdb',
|
|
||||||
'sap',
|
|
||||||
'mariadb',
|
|
||||||
'sqlite',
|
|
||||||
'cordova',
|
|
||||||
'react-native',
|
|
||||||
'nativescript',
|
|
||||||
'sqljs',
|
|
||||||
'oracle',
|
|
||||||
'mssql',
|
|
||||||
'mongodb',
|
|
||||||
'aurora-mysql',
|
|
||||||
'aurora-postgres',
|
|
||||||
'expo',
|
|
||||||
'better-sqlite3',
|
|
||||||
'capacitor',
|
|
||||||
'spanner',
|
|
||||||
]),
|
|
||||||
host: z.string(),
|
|
||||||
port: z.number().optional(),
|
|
||||||
username: z.string(),
|
|
||||||
password: z.string(),
|
|
||||||
database: z.string(),
|
|
||||||
secure: z.boolean(),
|
|
||||||
});
|
|
||||||
export type DatabaseConfig = z.TypeOf<typeof databaseSchema>;
|
|
||||||
|
|
||||||
export const rawDatabaseSchema = z.object({
|
|
||||||
connectionString: z.string(),
|
|
||||||
secure: z.boolean().default(true),
|
|
||||||
});
|
|
||||||
export type RawDatabaseConfigSchema = z.TypeOf<typeof rawDatabaseSchema>;
|
|
||||||
|
|
||||||
export const databaseConfigFactory = (opts: RawDatabaseConfigSchema) => registerAs('database', () => {
|
|
||||||
const connectionString = new ConnectionString(opts.connectionString);
|
|
||||||
const config: DatabaseConfig = databaseSchema.strict().parse({
|
|
||||||
connectionString: connectionString.toString(),
|
|
||||||
type: connectionString.protocol,
|
|
||||||
host: first(connectionString.hosts)?.name,
|
|
||||||
port: first(connectionString.hosts)?.port,
|
|
||||||
username: connectionString.user,
|
|
||||||
password: connectionString.password,
|
|
||||||
database: first(connectionString.path),
|
|
||||||
secure: opts.secure,
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
});
|
|
@ -1,17 +0,0 @@
|
|||||||
import { DataSource, DataSourceOptions } from 'typeorm';
|
|
||||||
|
|
||||||
export const dataSources: DataSource[] = [];
|
|
||||||
|
|
||||||
export const typeormDataSourceFactory = (dataSources: DataSource[]) => async (options?: DataSourceOptions) => {
|
|
||||||
const dataSource = await new DataSource(options!).initialize();
|
|
||||||
dataSources.push(dataSource);
|
|
||||||
return dataSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const typeormTransactionalDataSourceFactory =
|
|
||||||
(name?: string) => async (options?: DataSourceOptions) => {
|
|
||||||
const tt = await import('typeorm-transactional');
|
|
||||||
const dataSource = tt.addTransactionalDataSource({ name: name, dataSource: new DataSource(options!) });
|
|
||||||
dataSources.push(dataSource);
|
|
||||||
return dataSource;
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
export const typeormEntitiesFromImport = async <T extends object>(entities: T) => {
|
|
||||||
return (Object.keys(entities) as Array<keyof typeof entities>).map(
|
|
||||||
(entity: keyof typeof entities) => entities[entity]
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,26 +0,0 @@
|
|||||||
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
|
|
||||||
import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
|
||||||
import { DatabaseConfig } from './database-config';
|
|
||||||
|
|
||||||
export const typeormModuleOptionsFactory = async (
|
|
||||||
databaseConfig: DatabaseConfig,
|
|
||||||
entities: any[],
|
|
||||||
name?: string
|
|
||||||
): Promise<TypeOrmModuleOptions> => {
|
|
||||||
return {
|
|
||||||
name: name,
|
|
||||||
type: databaseConfig.type as any,
|
|
||||||
host: databaseConfig.host,
|
|
||||||
port: databaseConfig.port,
|
|
||||||
username: databaseConfig.username,
|
|
||||||
password: databaseConfig.password,
|
|
||||||
database: databaseConfig.database,
|
|
||||||
entities: entities,
|
|
||||||
synchronize: false,
|
|
||||||
logging: process.env['NODE_ENV'] !== 'production',
|
|
||||||
namingStrategy: new SnakeNamingStrategy(),
|
|
||||||
options: {
|
|
||||||
...(!databaseConfig.secure ? { trustServerCertificate: true } : {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
|
|
||||||
export async function patchTypeOrm() {
|
|
||||||
(await import('typeorm-transactional')).initializeTransactionalContext();
|
|
||||||
(await import('typeorm-scoped')).patchSelectQueryBuilder();
|
|
||||||
};
|
|
@ -1,105 +0,0 @@
|
|||||||
import { Inject, Injectable, OnApplicationBootstrap } from '@nestjs/common';
|
|
||||||
import { differenceBy, get } from 'lodash';
|
|
||||||
import { DataSource, QueryFailedError } from 'typeorm';
|
|
||||||
import { APP_DATASOURCES } from '../database/database.constants';
|
|
||||||
import { Enumify, registeredEnums } from './enumify';
|
|
||||||
import { EnumifyConfig, enumifyConfig } from './enumify.config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class EnumifyUpdateFromDatabaseService implements OnApplicationBootstrap {
|
|
||||||
constructor(
|
|
||||||
@Inject(APP_DATASOURCES) private readonly datasources: DataSource[],
|
|
||||||
@Inject(enumifyConfig.KEY) private readonly config: EnumifyConfig
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onApplicationBootstrap() {
|
|
||||||
try {
|
|
||||||
const enumUpdateFromDbOnStart = this.config.shouldUpdateEnumFromDbOnStartup;
|
|
||||||
if (!enumUpdateFromDbOnStart) return;
|
|
||||||
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
for (const dataSource of this.datasources) {
|
|
||||||
const logging = dataSource.options.logging;
|
|
||||||
dataSource.setOptions({ logging: false });
|
|
||||||
for (const _registeredEnum of get(registeredEnums, dataSource.name, [])) {
|
|
||||||
const registeredEnum = _registeredEnum as typeof Enumify;
|
|
||||||
|
|
||||||
const repo = dataSource.getRepository(registeredEnum);
|
|
||||||
|
|
||||||
const enumValues = registeredEnum.enumValues;
|
|
||||||
let dbValues: Enumify[];
|
|
||||||
try {
|
|
||||||
dbValues = await repo.find();
|
|
||||||
} catch (err) {
|
|
||||||
if (
|
|
||||||
err instanceof QueryFailedError &&
|
|
||||||
err.message === `Error: Invalid object name '${repo.metadata.tableName}'.`
|
|
||||||
) {
|
|
||||||
errors.push(
|
|
||||||
`[${dataSource.name}] ${registeredEnum.name}: Table present in code but missing on database: ${enumValues}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (err instanceof QueryFailedError && err.message.startsWith('Error: Invalid column name')) {
|
|
||||||
errors.push(`[${dataSource.name}] ${registeredEnum.name}: [${repo.metadata.tableName}] ${err.message}`);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const differenceByDbValues = differenceBy(dbValues, enumValues, (x) => (x.id, x.nome));
|
|
||||||
const differenceByEnumValues = differenceBy(enumValues, dbValues, (x) => (x.id, x.nome));
|
|
||||||
if (differenceByDbValues.length > 0) {
|
|
||||||
errors.push(
|
|
||||||
`[${dataSource.name}] ${
|
|
||||||
registeredEnum.name
|
|
||||||
}: Values present on database but missing in code: ${differenceBy(
|
|
||||||
dbValues,
|
|
||||||
enumValues,
|
|
||||||
(x) => (x.id, x.nome)
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (differenceByEnumValues.length > 0) {
|
|
||||||
errors.push(
|
|
||||||
`[${dataSource.name}] ${
|
|
||||||
registeredEnum.name
|
|
||||||
}: Values present in code but missing in database: ${differenceBy(
|
|
||||||
enumValues,
|
|
||||||
dbValues,
|
|
||||||
(x) => (x.id, x.nome)
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const dbValue of dbValues) {
|
|
||||||
const valueOfByName = registeredEnum.fromKey(dbValue.nome!);
|
|
||||||
const keyOfByCode = registeredEnum.fromValue(dbValue.id!);
|
|
||||||
if (valueOfByName != null && dbValue.id != valueOfByName?.id) {
|
|
||||||
errors.push(
|
|
||||||
`[${dataSource.name}] ${registeredEnum.name}: Different values between database (${dbValue.id}, ${dbValue.nome}) and code (${valueOfByName.id}, ${valueOfByName.nome})`
|
|
||||||
);
|
|
||||||
} else if (keyOfByCode != null && dbValue.nome != keyOfByCode?.nome) {
|
|
||||||
errors.push(
|
|
||||||
`[${dataSource.name}] ${registeredEnum.name}: Different values between database (${dbValue.id}, ${dbValue.nome}) and code (${keyOfByCode.id}, ${keyOfByCode.nome})`
|
|
||||||
);
|
|
||||||
} else if (valueOfByName != null || keyOfByCode != null) {
|
|
||||||
const enumValue = (valueOfByName ?? keyOfByCode)!;
|
|
||||||
Object.assign(enumValue, dbValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataSource.setOptions({ logging: logging });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
throw new Error(errors.join('\n\t* '));
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
console.warn(err.message, EnumifyUpdateFromDatabaseService.name);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import coerceRecordTypes from '../config/utils/coerce-record-types';
|
|
||||||
import { registerAs } from '@nestjs/config';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const enumifySchema = z.object({
|
|
||||||
shouldUpdateEnumFromDbOnStartup: z.boolean().default(true),
|
|
||||||
});
|
|
||||||
export type EnumifyConfig = z.infer<typeof enumifySchema>;
|
|
||||||
|
|
||||||
export const enumifyConfig = registerAs('enumify', () => {
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
|
|
||||||
const config: EnumifyConfig = enumifySchema.strict().parse({
|
|
||||||
shouldUpdateEnumFromDbOnStartup: env['ENUM_UPDATE_FROM_DB_ON_START']
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
});
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Module, Global } from '@nestjs/common';
|
|
||||||
import { EnumifyUpdateFromDatabaseService } from './enumify-update-from-database.service';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { enumifyConfig } from './enumify.config';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
imports: [ConfigModule.forFeature(enumifyConfig)],
|
|
||||||
providers: [EnumifyUpdateFromDatabaseService],
|
|
||||||
exports: [EnumifyUpdateFromDatabaseService],
|
|
||||||
})
|
|
||||||
export class EnumifyModule {}
|
|
@ -1,74 +0,0 @@
|
|||||||
// Based on https://github.com/rauschma/enumify
|
|
||||||
// Original license: MIT License - Copyright (c) 2020 Axel Rauschmayer
|
|
||||||
|
|
||||||
export const registeredEnums: Record<string, typeof Enumify[]> = {};
|
|
||||||
export function Enum(schema: string) {
|
|
||||||
return function (target: typeof Enumify) {
|
|
||||||
registeredEnums[schema] ??= [];
|
|
||||||
registeredEnums[schema].push(target);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Enumify {
|
|
||||||
static enumKeys: Array<string>;
|
|
||||||
static enumValues: Array<Enumify>;
|
|
||||||
|
|
||||||
public readonly id?: number;
|
|
||||||
public readonly nome?: string;
|
|
||||||
|
|
||||||
static closeEnum() {
|
|
||||||
const enumKeys: Array<string> = [];
|
|
||||||
const enumValues: Array<Enumify> = [];
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(this)) {
|
|
||||||
value.nome ??= key;
|
|
||||||
value.id ??= enumValues.length;
|
|
||||||
|
|
||||||
if (value.id == null || value.id === '') {
|
|
||||||
throw new Error(`${this.name}.id`);
|
|
||||||
}
|
|
||||||
if (value.nome == null || value.nome === '') {
|
|
||||||
throw new Error(`${this.name}.nome`);
|
|
||||||
}
|
|
||||||
|
|
||||||
enumKeys.push(value.nome);
|
|
||||||
enumValues.push(value);
|
|
||||||
}
|
|
||||||
this.enumKeys = enumKeys;
|
|
||||||
this.enumValues = enumValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromKey(key: string): undefined | Enumify {
|
|
||||||
if (this.enumKeys == undefined) {
|
|
||||||
throw new Error(`Did you forget to call static _ = ${this.name}.closeEnum() after setup?`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this.enumKeys.findIndex((enumKey) => enumKey.toUpperCase() === key.toString().toUpperCase());
|
|
||||||
if (index >= 0) {
|
|
||||||
return this.enumValues[index];
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromValue(value: number): undefined | Enumify {
|
|
||||||
if (this.enumValues == undefined) {
|
|
||||||
throw new Error(`Did you forget to call static _ = ${this.name}.closeEnum() after setup?`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = this.enumValues.map((x) => x.id).indexOf(value);
|
|
||||||
if (index >= 0) {
|
|
||||||
const key = this.enumKeys[index];
|
|
||||||
return this.fromKey(key);
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected constructor(id?: number, nome?: string) {
|
|
||||||
this.id = id;
|
|
||||||
this.nome = nome;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return `(${this.id}, ${this.nome})`;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,137 +0,0 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
|
||||||
import fs from 'node:fs';
|
|
||||||
import { randomUUID } from 'node:crypto';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { FileTransactionsService } from './file-transactions.service';
|
|
||||||
import {
|
|
||||||
copyFilesToFolderOrFail,
|
|
||||||
deleteFiles,
|
|
||||||
renameFiles,
|
|
||||||
renameFilesOrFail,
|
|
||||||
} from './file-utils';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
export class FileTransaction {
|
|
||||||
public static readonly TO_ADD_SUFFIX = 'toAdd';
|
|
||||||
public static readonly TO_DELETE_SUFFIX = 'toDelete';
|
|
||||||
|
|
||||||
private readonly manager: FileTransactionsService;
|
|
||||||
private readonly logger: Logger;
|
|
||||||
|
|
||||||
public readonly transactionId: string;
|
|
||||||
private readonly ts: Date;
|
|
||||||
public readonly filesToAdd: { src: string; dest: string }[];
|
|
||||||
public readonly filesToDelete: string[];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
manager: FileTransactionsService,
|
|
||||||
opts: Partial<{
|
|
||||||
transactionId: string;
|
|
||||||
filesToAdd: { src: string; dest: string }[];
|
|
||||||
filesToDelete: string[];
|
|
||||||
logger: Logger;
|
|
||||||
}>
|
|
||||||
) {
|
|
||||||
this.manager = manager;
|
|
||||||
this.logger = opts.logger ?? new Logger(FileTransaction.name);
|
|
||||||
this.filesToAdd = opts.filesToAdd ?? [];
|
|
||||||
this.filesToDelete = opts.filesToDelete ?? [];
|
|
||||||
this.transactionId = opts.transactionId ?? randomUUID().split('-').pop() as string;
|
|
||||||
this.ts = new Date();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addFile(src: string, dest: string) {
|
|
||||||
const destFolderName = path.dirname(dest);
|
|
||||||
const destFileName = path.basename(dest);
|
|
||||||
const destFileNameWithSuffix = `${destFileName}.${this.transactionId}.${FileTransaction.TO_ADD_SUFFIX}`;
|
|
||||||
const destFilePathWithSuffix = path.join(
|
|
||||||
destFolderName,
|
|
||||||
destFileNameWithSuffix
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Adding file '${destFilePathWithSuffix}' in fileTransaction '${
|
|
||||||
this.transactionId
|
|
||||||
}' (started at ${dayjs(this.ts).format('YYYY-MM-DD HH:mm:ss')})`
|
|
||||||
);
|
|
||||||
this.filesToAdd.push({ src: src, dest: destFilePathWithSuffix });
|
|
||||||
|
|
||||||
if (fs.existsSync(dest)) {
|
|
||||||
await this.deleteFile(dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
await copyFilesToFolderOrFail(this.logger, destFolderName, {
|
|
||||||
srcpath: src,
|
|
||||||
name: destFileNameWithSuffix,
|
|
||||||
});
|
|
||||||
await deleteFiles(this.logger, src);
|
|
||||||
this.manager.persist(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteFile(src: string) {
|
|
||||||
const destFolderName = path.dirname(src);
|
|
||||||
const destFileName = path.basename(src);
|
|
||||||
const destFileNameWithSuffix = `${destFileName}.${this.transactionId}.${FileTransaction.TO_DELETE_SUFFIX}`;
|
|
||||||
const destFilePathWithSuffix = path.join(
|
|
||||||
destFolderName,
|
|
||||||
destFileNameWithSuffix
|
|
||||||
);
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Deleting file '${destFilePathWithSuffix}' in fileTransaction '${
|
|
||||||
this.transactionId
|
|
||||||
}' (started at ${dayjs(this.ts).format('YYYY-MM-DD HH:mm:ss')})`
|
|
||||||
);
|
|
||||||
this.filesToDelete.push(destFilePathWithSuffix);
|
|
||||||
await renameFilesOrFail(this.logger, { src, dest: destFilePathWithSuffix });
|
|
||||||
this.manager.persist(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async commitTransaction() {
|
|
||||||
this.logger.log(
|
|
||||||
`Committing fileTransaction '${this.transactionId}' (started at ${dayjs(
|
|
||||||
this.ts
|
|
||||||
).format('YYYY-MM-DD HH:mm:ss')})`
|
|
||||||
);
|
|
||||||
await renameFiles(
|
|
||||||
this.logger,
|
|
||||||
...this.filesToAdd.map((fileToAdd) => ({
|
|
||||||
src: fileToAdd.dest,
|
|
||||||
dest: fileToAdd.dest.replace(
|
|
||||||
`.${this.transactionId}.${FileTransaction.TO_ADD_SUFFIX}`,
|
|
||||||
''
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
await deleteFiles(this.logger, ...this.filesToDelete);
|
|
||||||
this.manager.unregister(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async rollbackTransaction() {
|
|
||||||
this.logger.log(
|
|
||||||
`Rollbacking fileTransaction '${this.transactionId}' (started at ${dayjs(
|
|
||||||
this.ts
|
|
||||||
).format('YYYY-MM-DD HH:mm:ss')})`
|
|
||||||
);
|
|
||||||
|
|
||||||
await renameFiles(
|
|
||||||
this.logger,
|
|
||||||
...this.filesToAdd.map((fileToMove) => ({
|
|
||||||
src: fileToMove.dest,
|
|
||||||
dest: fileToMove.src,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
await renameFiles(
|
|
||||||
this.logger,
|
|
||||||
...this.filesToDelete.map((fileToRestore) => ({
|
|
||||||
src: fileToRestore,
|
|
||||||
dest: fileToRestore.replace(
|
|
||||||
`.${this.transactionId}.${FileTransaction.TO_DELETE_SUFFIX}`,
|
|
||||||
''
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
this.manager.unregister(this);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Global, Module } from '@nestjs/common';
|
|
||||||
import { KeyvModule } from '../keyv/keyv.module';
|
|
||||||
import { FileTransactionsService } from './file-transactions.service';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
imports: [KeyvModule],
|
|
||||||
providers: [FileTransactionsService],
|
|
||||||
exports: [FileTransactionsService],
|
|
||||||
})
|
|
||||||
export class AppFileTransactionsModule {}
|
|
@ -1,54 +0,0 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
||||||
import { FileTransaction } from './file-transaction';
|
|
||||||
import { createKeyv, Keyv } from '@keyv/redis';
|
|
||||||
import { parse, stringify } from 'flatted';
|
|
||||||
import { KeyvService } from '../keyv/keyv.service';
|
|
||||||
|
|
||||||
const FILE_TRANSACTIONS_KEY = `FILE_TRANSACTIONS_KEY`;
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class FileTransactionsService implements OnModuleInit {
|
|
||||||
private readonly logger = new Logger(FileTransactionsService.name);
|
|
||||||
private readonly map: Record<string, FileTransaction> = {};
|
|
||||||
|
|
||||||
private readonly keyv: Keyv;
|
|
||||||
|
|
||||||
constructor(readonly keyvService: KeyvService) {
|
|
||||||
this.keyv = this.keyvService.create({
|
|
||||||
namespace: FileTransactionsService.name,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
register(logger?: Logger) {
|
|
||||||
const tx = new FileTransaction(this, { logger });
|
|
||||||
this.persist(tx);
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
persist(tx: FileTransaction) {
|
|
||||||
this.map[tx.transactionId] = tx;
|
|
||||||
this.keyv.set(FILE_TRANSACTIONS_KEY, this.map);
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(tx: FileTransaction) {
|
|
||||||
delete this.map[tx.transactionId];
|
|
||||||
this.keyv.set(FILE_TRANSACTIONS_KEY, this.map);
|
|
||||||
}
|
|
||||||
|
|
||||||
async onModuleInit() {
|
|
||||||
this.logger.log('Checking for dangling file transactions...');
|
|
||||||
const dangling = await this.keyv.get<Set<FileTransaction>>(
|
|
||||||
FILE_TRANSACTIONS_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
if (dangling) {
|
|
||||||
for (const danglingTx of Object.values(dangling)) {
|
|
||||||
const tx = new FileTransaction(this, {
|
|
||||||
...danglingTx,
|
|
||||||
logger: this.logger,
|
|
||||||
});
|
|
||||||
await tx.rollbackTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
|
||||||
import { promises as fs } from 'node:fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
interface File {
|
|
||||||
srcpath: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const copyFilesToFolder = (logger: Logger, destFolder: string, ...files: File[]) =>
|
|
||||||
_copyFilesToFolder(logger, destFolder, false, ...files);
|
|
||||||
export const copyFilesToFolderOrFail = (logger: Logger, destFolder: string, ...files: File[]) =>
|
|
||||||
_copyFilesToFolder(logger, destFolder, true, ...files);
|
|
||||||
export const deleteFolder = (logger: Logger, folder: string) => _deleteFolder(logger, false, folder);
|
|
||||||
export const deleteFolderOrFail = (logger: Logger, folder: string) => _deleteFolder(logger, true, folder);
|
|
||||||
export const deleteFiles = (logger: Logger, ...files: string[]) => _deleteFiles(logger, false, ...files);
|
|
||||||
export const deleteFilesOrFail = (logger: Logger, ...files: string[]) => _deleteFiles(logger, true, ...files);
|
|
||||||
export const renameFiles = (logger: Logger, ...files: Array<{ src: string; dest: string }>) =>
|
|
||||||
_renameFiles(logger, false, ...files);
|
|
||||||
export const renameFilesOrFail = (logger: Logger, ...files: Array<{ src: string; dest: string }>) =>
|
|
||||||
_renameFiles(logger, true, ...files);
|
|
||||||
|
|
||||||
export const normalizeWin32PathToPosix = (filepath: string): string =>
|
|
||||||
path.win32.normalize(filepath).split(path.win32.sep).join(path.posix.sep);
|
|
||||||
|
|
||||||
const _copyFilesToFolder = async (
|
|
||||||
logger: Logger,
|
|
||||||
destFolder: string,
|
|
||||||
throwsOnError = false,
|
|
||||||
...files: File[]
|
|
||||||
): Promise<void> => {
|
|
||||||
await fs.mkdir(destFolder, { recursive: true });
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
const destpath = path.join(destFolder, file.name);
|
|
||||||
await fs.copyFile(file.srcpath, destpath);
|
|
||||||
logger.debug(`Copying file '${file.srcpath}' to '${destpath}'`);
|
|
||||||
} catch (err: any) {
|
|
||||||
const msg = `Could not copy '${file.srcpath}' to '${destFolder}'. Inner exception: ${err.message}`;
|
|
||||||
if (throwsOnError) {
|
|
||||||
logger.error(msg);
|
|
||||||
err.message = msg;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
logger.warn(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _deleteFolder = async (logger: Logger, throwsOnError = false, folder: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await fs.rmdir(folder, { recursive: true });
|
|
||||||
logger.debug(`Deleting folder '${folder}'`);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const msg = `Could not delete '${folder}'. Inner exception: ${err.message}`;
|
|
||||||
if (throwsOnError) {
|
|
||||||
logger.error(msg);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
logger.warn(msg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _deleteFiles = async (logger: Logger, throwsOnError = false, ...files: string[]): Promise<void> => {
|
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
await fs.unlink(file);
|
|
||||||
logger.debug(`Deleting file '${file}'`);
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err.code === 'ENOENT') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const msg = `Could not delete '${file}'. Inner exception: ${err.message}`;
|
|
||||||
if (throwsOnError) {
|
|
||||||
logger.error(msg);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
logger.warn(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const _renameFiles = async (
|
|
||||||
logger: Logger,
|
|
||||||
throwsOnError = false,
|
|
||||||
...files: Array<{ src: string; dest: string }>
|
|
||||||
): Promise<void> => {
|
|
||||||
for (const file of files) {
|
|
||||||
try {
|
|
||||||
await fs.rename(file.src, file.dest);
|
|
||||||
logger.debug(`Renaming file '${file.src}' to '${file.dest}'`);
|
|
||||||
} catch (err: any) {
|
|
||||||
const msg = `Could not rename '${file.src}' to '${file.dest}'. Inner exception: ${err.message}`;
|
|
||||||
if (throwsOnError) {
|
|
||||||
logger.error(msg);
|
|
||||||
err.message = msg;
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
logger.warn(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,30 +0,0 @@
|
|||||||
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
|
|
||||||
import {
|
|
||||||
DiskHealthIndicator,
|
|
||||||
HealthCheck,
|
|
||||||
HealthCheckService,
|
|
||||||
MemoryHealthIndicator,
|
|
||||||
TypeOrmHealthIndicator,
|
|
||||||
} from '@nestjs/terminus';
|
|
||||||
import { Public } from '../auth/strategies/jwt/jwt-auth.guard';
|
|
||||||
|
|
||||||
@Public()
|
|
||||||
@Controller({ path: 'health', version: VERSION_NEUTRAL })
|
|
||||||
export class HealthController {
|
|
||||||
constructor(
|
|
||||||
private readonly health: HealthCheckService,
|
|
||||||
private readonly db: TypeOrmHealthIndicator,
|
|
||||||
private readonly disk: DiskHealthIndicator,
|
|
||||||
private readonly memory: MemoryHealthIndicator
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@HealthCheck()
|
|
||||||
check() {
|
|
||||||
return this.health.check([
|
|
||||||
() => this.db.pingCheck('database'),
|
|
||||||
() => this.disk.checkStorage('storage', { path: '/', thresholdPercent: 0.5 }), // Threshold at 50% space occupied in path,
|
|
||||||
() => this.memory.checkHeap('memory', 150 * 1024 * 1024), // Threshold at 150MB memory occupied by process
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { TerminusModule } from '@nestjs/terminus';
|
|
||||||
import { HealthController } from './health.controller';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TerminusModule.forRoot({
|
|
||||||
errorLogStyle: 'pretty',
|
|
||||||
})],
|
|
||||||
controllers: [HealthController],
|
|
||||||
})
|
|
||||||
export class AppHealthModule {}
|
|
@ -1,17 +0,0 @@
|
|||||||
import coerceRecordTypes from '../config/utils/coerce-record-types';
|
|
||||||
import { registerAs } from '@nestjs/config';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const keyvSchema = z.object({
|
|
||||||
redis: z.string(),
|
|
||||||
});
|
|
||||||
export type KeyvConfig = z.infer<typeof keyvSchema>;
|
|
||||||
|
|
||||||
export const keyvConfig = registerAs('keyv', () => {
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
|
|
||||||
const config: KeyvConfig = keyvSchema.strict().parse({
|
|
||||||
redis: env['REDIS_CONNECTION_STRING'],
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
});
|
|
@ -1,11 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
import { keyvConfig } from './keyv.config';
|
|
||||||
import { KeyvService } from './keyv.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [ConfigModule.forFeature(keyvConfig)],
|
|
||||||
providers: [KeyvService],
|
|
||||||
exports: [KeyvService],
|
|
||||||
})
|
|
||||||
export class KeyvModule {}
|
|
@ -1,16 +0,0 @@
|
|||||||
import { createKeyv, KeyvRedisOptions } from '@keyv/redis';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { parse, stringify } from 'flatted';
|
|
||||||
import { keyvConfig, KeyvConfig } from './keyv.config';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class KeyvService {
|
|
||||||
constructor(@Inject(keyvConfig.KEY) private readonly config: KeyvConfig) {}
|
|
||||||
|
|
||||||
create(options?: KeyvRedisOptions) {
|
|
||||||
const keyv = createKeyv(this.config.redis, options);
|
|
||||||
keyv.serialize = stringify;
|
|
||||||
keyv.deserialize = parse;
|
|
||||||
return keyv;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import coerceRecordTypes from '../config/utils/coerce-record-types';
|
|
||||||
import { registerAs } from '@nestjs/config';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
export const loggerSchema = z.object({
|
|
||||||
seqServerHost: z.string(),
|
|
||||||
seqServerPort: z.number().finite().positive(),
|
|
||||||
seqApiKey: z.string(),
|
|
||||||
});
|
|
||||||
export type LoggerConfig = z.infer<typeof loggerSchema>;
|
|
||||||
|
|
||||||
export const loggerConfig = registerAs('logger', () => {
|
|
||||||
const env = coerceRecordTypes(process.env);
|
|
||||||
|
|
||||||
const config: LoggerConfig = loggerSchema.strict().parse({
|
|
||||||
seqServerHost: env['SEQ_SERVER_HOST'],
|
|
||||||
seqServerPort: env['SEQ_SERVER_PORT'],
|
|
||||||
seqApiKey: env['SEQ_API_KEY'],
|
|
||||||
});
|
|
||||||
return config;
|
|
||||||
});
|
|
@ -1,59 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { NestApplication } from '@nestjs/core';
|
|
||||||
import { LoggerModule } from 'nestjs-pino';
|
|
||||||
import { LoggerConfig, loggerConfig } from './logger.config';
|
|
||||||
import { ConfigModule } from '@nestjs/config';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [
|
|
||||||
ConfigModule.forFeature(loggerConfig),
|
|
||||||
LoggerModule.forRootAsync({
|
|
||||||
imports: loggerConfig.asProvider().imports,
|
|
||||||
useFactory: (loggerConfig: LoggerConfig) => ({
|
|
||||||
pinoHttp: {
|
|
||||||
autoLogging: true,
|
|
||||||
quietReqLogger: true,
|
|
||||||
quietResLogger: true,
|
|
||||||
level: 'trace',
|
|
||||||
useLevel: 'trace',
|
|
||||||
mixin() {
|
|
||||||
return { context: NestApplication.name };
|
|
||||||
},
|
|
||||||
customSuccessMessage: (req, res) => { return `${req.method} ${req.url}: ${res.statusCode} request succedeed` },
|
|
||||||
customErrorMessage: (req, res) => { return `${req.method} ${req.url}: ${res.statusCode} request errored` },
|
|
||||||
name: 'ebitemp-api',
|
|
||||||
transport: {
|
|
||||||
targets: [
|
|
||||||
...(process.env['NODE_ENV'] !== 'production'
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
target: 'pino-pretty',
|
|
||||||
level: 'debug',
|
|
||||||
options: {
|
|
||||||
colorize: true,
|
|
||||||
messageFormat: '[{context}] {msg}',
|
|
||||||
ignore: 'pid,hostname,context',
|
|
||||||
translateTime: 'SYS:yyyy-mm-dd HH:MM:ss o',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
target: '@autotelic/pino-seq-transport',
|
|
||||||
level: 'trace',
|
|
||||||
options: {
|
|
||||||
loggerOpts: {
|
|
||||||
serverUrl: `${loggerConfig.seqServerHost}:${loggerConfig.seqServerPort}`,
|
|
||||||
apiKey: loggerConfig.seqApiKey,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
inject: [loggerConfig.KEY],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppLoggerModule {}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
|
||||||
import { TipiJobsEntity } from '../database/connections/ebitemp-api/entities';
|
|
||||||
import { SchedulerService } from './schedule.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [TypeOrmModule.forFeature([TipiJobsEntity]), ScheduleModule.forRoot()],
|
|
||||||
providers: [SchedulerService],
|
|
||||||
exports: [],
|
|
||||||
})
|
|
||||||
export class AppScheduleModule {}
|
|
@ -1,54 +0,0 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
|
||||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
|
||||||
import { CronJob, CronTime } from 'cron';
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { TipiJobsEntity } from '../database/connections/ebitemp-api/entities/tipi_jobs.entity';
|
|
||||||
import { isError } from '@stdlib/assert';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class SchedulerService {
|
|
||||||
private readonly logger = new Logger(SchedulerService.name);
|
|
||||||
|
|
||||||
private static readonly TIME_ZONE = 'Europe/Rome';
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly schedulerRegistry: SchedulerRegistry,
|
|
||||||
@InjectRepository(TipiJobsEntity) private readonly tipiJobsRepository: Repository<TipiJobsEntity>
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async onApplicationBootstrap() {
|
|
||||||
try {
|
|
||||||
const jobs = this.schedulerRegistry.getCronJobs();
|
|
||||||
const jobTypes = await this.tipiJobsRepository.find();
|
|
||||||
|
|
||||||
for (const [jobName, job] of jobs) {
|
|
||||||
this.rescheduleJobAccordingToJobType(jobTypes, jobName, job);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
if (!isError(err)) throw err;
|
|
||||||
this.logger.error(err.message);
|
|
||||||
this.logger.warn(`Error while retrieving scheduled jobs on database. Skipping jobs.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private rescheduleJobAccordingToJobType(jobTypes: TipiJobsEntity[], jobName: string, job: CronJob) {
|
|
||||||
const jobType = jobTypes.find((jobType) => jobType.nome == jobName);
|
|
||||||
|
|
||||||
if (!jobType) {
|
|
||||||
this.logger.warn(`Job type for job '${jobName}' not found on database. Skipping job.`);
|
|
||||||
this.schedulerRegistry.deleteCronJob(jobName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const jobTime = new CronTime(jobType.patternCron, SchedulerService.TIME_ZONE);
|
|
||||||
job.setTime(jobTime);
|
|
||||||
job.start();
|
|
||||||
|
|
||||||
this.logger.log(
|
|
||||||
`Job type id '${jobType.id}' found for job '${jobName}' [isActive: ${jobType.flagAttivo}, cronPattern: ${
|
|
||||||
jobType.patternCron
|
|
||||||
}]. Upcoming date => ${job.nextDate()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
export async function patchNestjsSwagger() {
|
|
||||||
(await import('@anatine/zod-nestjs')).patchNestjsSwagger();
|
|
||||||
};
|
|
@ -1,12 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
|
|
||||||
import { ZodValidationPipe } from './zod.pipe';
|
|
||||||
import { ZodSerializerInterceptor } from './zod.serializer';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [
|
|
||||||
{ provide: APP_PIPE, useClass: ZodValidationPipe },
|
|
||||||
{ provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor },
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class AppValidationModule {}
|
|
@ -1,36 +0,0 @@
|
|||||||
import { ZodDtoStatic } from '@anatine/zod-nestjs';
|
|
||||||
import { HTTP_ERRORS_BY_CODE } from '@anatine/zod-nestjs/src/lib/http-errors';
|
|
||||||
|
|
||||||
import { ArgumentMetadata, HttpStatus, Injectable, Optional, PipeTransform } from '@nestjs/common';
|
|
||||||
|
|
||||||
export interface ZodValidationPipeOptions {
|
|
||||||
errorHttpStatusCode?: keyof typeof HTTP_ERRORS_BY_CODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ZodValidationPipe implements PipeTransform {
|
|
||||||
private readonly errorHttpStatusCode: keyof typeof HTTP_ERRORS_BY_CODE;
|
|
||||||
|
|
||||||
constructor(@Optional() options?: ZodValidationPipeOptions) {
|
|
||||||
this.errorHttpStatusCode = options?.errorHttpStatusCode || HttpStatus.BAD_REQUEST;
|
|
||||||
}
|
|
||||||
|
|
||||||
public transform(value: unknown, metadata: ArgumentMetadata): unknown {
|
|
||||||
const zodSchema = (metadata?.metatype as ZodDtoStatic)?.zodSchema;
|
|
||||||
|
|
||||||
if (zodSchema) {
|
|
||||||
const parseResult = zodSchema.safeParse(value);
|
|
||||||
|
|
||||||
if (!parseResult.success) {
|
|
||||||
const { error } = parseResult;
|
|
||||||
const message = error.errors.map((error) => `${error.path.join('.')}: ${error.message}`);
|
|
||||||
|
|
||||||
throw new HTTP_ERRORS_BY_CODE[this.errorHttpStatusCode](message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseResult.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
import { ZodDtoStatic } from '@anatine/zod-nestjs';
|
|
||||||
import { HTTP_ERRORS_BY_CODE } from '@anatine/zod-nestjs/src/lib/http-errors';
|
|
||||||
import {
|
|
||||||
CallHandler,
|
|
||||||
ExecutionContext,
|
|
||||||
HttpStatus,
|
|
||||||
Inject,
|
|
||||||
Injectable,
|
|
||||||
NestInterceptor,
|
|
||||||
Optional,
|
|
||||||
SetMetadata,
|
|
||||||
StreamableFile,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { chain } from 'lodash';
|
|
||||||
import { Observable, map } from 'rxjs';
|
|
||||||
import { ZodSchema } from 'zod';
|
|
||||||
|
|
||||||
const REFLECTOR = 'Reflector';
|
|
||||||
|
|
||||||
export interface ZodSerializerInterceptorOptions {
|
|
||||||
errorHttpStatusCode?: keyof typeof HTTP_ERRORS_BY_CODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ZodSerializerDto = (dto: ZodDtoStatic | ZodSchema) => SetMetadata('zodSerializedDtoOptions', { dto });
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ZodSerializerInterceptor implements NestInterceptor {
|
|
||||||
private readonly errorHttpStatusCode: keyof typeof HTTP_ERRORS_BY_CODE;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
@Inject(REFLECTOR) protected readonly reflector: any,
|
|
||||||
@Optional() options?: ZodSerializerInterceptorOptions
|
|
||||||
) {
|
|
||||||
this.errorHttpStatusCode = options?.errorHttpStatusCode || HttpStatus.INTERNAL_SERVER_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
|
||||||
const responseSchema = this.getContextResponseSchema(context);
|
|
||||||
|
|
||||||
return next.handle().pipe(
|
|
||||||
map((res: object | object[]) => {
|
|
||||||
if (!responseSchema) return res;
|
|
||||||
if (typeof res !== 'object' || res instanceof StreamableFile) return res;
|
|
||||||
|
|
||||||
return Array.isArray(res)
|
|
||||||
? res.map(
|
|
||||||
(item) =>
|
|
||||||
validateInterally(responseSchema, item, { httpStatusCodeOnError: this.errorHttpStatusCode }).data
|
|
||||||
)
|
|
||||||
: validateInterally(responseSchema, res, { httpStatusCodeOnError: this.errorHttpStatusCode }).data;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected getContextResponseSchema(context: ExecutionContext): ZodDtoStatic | ZodSchema | undefined {
|
|
||||||
const zodSerializedDto = (() => {
|
|
||||||
const metadata = this.reflector.getAllAndMerge('zodSerializedDtoOptions', [context.getHandler(), context.getClass()]);
|
|
||||||
return metadata.dto;
|
|
||||||
})();
|
|
||||||
if (zodSerializedDto) return zodSerializedDto;
|
|
||||||
|
|
||||||
const swaggerApiResponseType = (() => {
|
|
||||||
const metadata = this.reflector.getAllAndMerge('swagger/apiResponse', [context.getHandler(), context.getClass()]);
|
|
||||||
const metadataForStatusCode = chain([metadata[HttpStatus.OK], metadata[HttpStatus.CREATED]]).filter(Boolean).first().value();
|
|
||||||
return metadataForStatusCode.type;
|
|
||||||
})();
|
|
||||||
if (swaggerApiResponseType) return swaggerApiResponseType;
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isZodDto(metatype: unknown): metatype is ZodDtoStatic<any> {
|
|
||||||
return !!(metatype as ZodDtoStatic)?.zodSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateInterally(
|
|
||||||
schemaOrDto: ZodDtoStatic | ZodSchema,
|
|
||||||
value: any,
|
|
||||||
options: Partial<{
|
|
||||||
httpStatusCodeOnError: keyof typeof HTTP_ERRORS_BY_CODE;
|
|
||||||
}>
|
|
||||||
) {
|
|
||||||
const { httpStatusCodeOnError = HttpStatus.INTERNAL_SERVER_ERROR } = options;
|
|
||||||
let schema = isZodDto(schemaOrDto) ? schemaOrDto.zodSchema : schemaOrDto;
|
|
||||||
|
|
||||||
const parseResult = schema.safeParse(value);
|
|
||||||
if (!parseResult.success) {
|
|
||||||
const { error } = parseResult;
|
|
||||||
const message = error.errors.map((error) => `${error.path.join('.')}: ${error.message}`);
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
`Error while validating dto '${isZodDto(schemaOrDto) ? schemaOrDto.name : 'unknownSchema'}': ${message}`
|
|
||||||
);
|
|
||||||
console.error('value', value);
|
|
||||||
throw new HTTP_ERRORS_BY_CODE[httpStatusCodeOnError]();
|
|
||||||
}
|
|
||||||
return parseResult;
|
|
||||||
}
|
|
@ -1,61 +1,19 @@
|
|||||||
import { Logger, RequestMethod, VersioningType } from '@nestjs/common';
|
/**
|
||||||
import { NestFactory } from '@nestjs/core';
|
* This is not a production server yet!
|
||||||
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
|
* This is only a minimal backend to get started.
|
||||||
import { Logger as PinoLogger } from 'nestjs-pino';
|
*/
|
||||||
|
|
||||||
import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger';
|
import { Logger } from '@nestjs/common';
|
||||||
import { flow } from 'lodash';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { AppModule } from './app/app.module';
|
import { AppModule } from './app/app.module';
|
||||||
import { patchPublicDecoratorSupport } from './app/modules/auth/strategies/jwt/jwt-auth.guard';
|
|
||||||
import { LocalConfig, localConfig } from './app/modules/config/local.config';
|
|
||||||
import { patchTypeOrm } from './app/modules/database/utils/typeorm-patch';
|
|
||||||
import { patchNestjsSwagger } from './app/modules/validation/utils/nestjs-swagger-patches';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
await patchTypeOrm();
|
const app = await NestFactory.create(AppModule);
|
||||||
await patchNestjsSwagger();
|
const globalPrefix = 'api';
|
||||||
|
app.setGlobalPrefix(globalPrefix);
|
||||||
const appOpts = (() => {
|
const port = process.env.PORT || 3000;
|
||||||
const loggerOpts = { bufferLogs: true };
|
|
||||||
return {
|
|
||||||
forceCloseConnections: true,
|
|
||||||
...loggerOpts,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(appOpts));
|
|
||||||
app.enableVersioning({
|
|
||||||
type: VersioningType.URI,
|
|
||||||
defaultVersion: '1'
|
|
||||||
});
|
|
||||||
app.enableShutdownHooks();
|
|
||||||
|
|
||||||
const config = app.get<LocalConfig>(localConfig.KEY);
|
|
||||||
const port = config.port || 3000;
|
|
||||||
|
|
||||||
app.useLogger(app.get(PinoLogger));
|
|
||||||
|
|
||||||
const swaggerConfig = new DocumentBuilder().addBearerAuth().build();
|
|
||||||
const swaggerOptions = {
|
|
||||||
operationIdFactory: (controllerKey: string, methodKey: string) => {
|
|
||||||
return `${controllerKey}_${methodKey}`;
|
|
||||||
},
|
|
||||||
} satisfies SwaggerDocumentOptions;
|
|
||||||
const document = flow(
|
|
||||||
() => SwaggerModule.createDocument(app, swaggerConfig, swaggerOptions),
|
|
||||||
patchPublicDecoratorSupport
|
|
||||||
)();
|
|
||||||
|
|
||||||
SwaggerModule.setup(`docs`, app, document, {
|
|
||||||
swaggerOptions: {
|
|
||||||
operationsSorter: 'alpha',
|
|
||||||
displayOperationId: true,
|
|
||||||
filter: true,
|
|
||||||
persistAuthorization: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await app.listen(port);
|
await app.listen(port);
|
||||||
Logger.log(`🚀 Application is running on: http://localhost:${port}`);
|
Logger.log(`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
void bootstrap();
|
bootstrap();
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||||
const { join, relative } = require('path');
|
const { join } = require('path');
|
||||||
|
|
||||||
const workspaceRoot = join(__dirname, '../../');
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
output: {
|
output: {
|
||||||
path: join(__dirname, '../../dist/apps/ebitemp-api'),
|
path: join(__dirname, '../../dist/apps/ebitemp-api'),
|
||||||
devtoolModuleFilenameTemplate(info) {
|
|
||||||
const rel = relative(workspaceRoot, info.absoluteResourcePath);
|
|
||||||
return `webpack:///./${rel}`;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new NxAppWebpackPlugin({
|
new NxAppWebpackPlugin({
|
||||||
@ -16,13 +11,10 @@ module.exports = {
|
|||||||
compiler: 'tsc',
|
compiler: 'tsc',
|
||||||
main: './src/main.ts',
|
main: './src/main.ts',
|
||||||
tsConfig: './tsconfig.app.json',
|
tsConfig: './tsconfig.app.json',
|
||||||
assets: ['./src/assets'],
|
assets: ["./src/assets"],
|
||||||
optimization: false,
|
optimization: false,
|
||||||
outputHashing: 'none',
|
outputHashing: 'none',
|
||||||
generatePackageJson: true,
|
generatePackageJson: true,
|
||||||
transformers: [
|
})
|
||||||
{ name: '@nestjs/swagger/plugin', options: { introspectComments: true } }
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
services:
|
|
||||||
seq:
|
|
||||||
image: datalust/seq:latest
|
|
||||||
ports:
|
|
||||||
- ${SEQ_UI_PORT}:80
|
|
||||||
- ${SEQ_SERVER_PORT}:5341
|
|
||||||
environment:
|
|
||||||
- ACCEPT_EULA=Y
|
|
||||||
- SEQ_FIRSTRUN_ADMINPASSWORDHASH=QCxh/LN52rQ/pGlajPfJpqdopsx7IX9KAi+5rcU2hETBxfjjHQXydaN9kO9h2n/yKB1DbxnmGULOSlfgsTZRbjUF/wQBrBpbELhUTybNwhAw
|
|
||||||
- BASE_URI=${SEQ_UI_HOST}:${SEQ_UI_PORT}
|
|
||||||
restart: unless-stopped
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 4096M
|
|
||||||
reservations:
|
|
||||||
memory: 2048M
|
|
||||||
redis:
|
|
||||||
image: redis:alpine
|
|
||||||
ports:
|
|
||||||
- '${REDIS_PORT}:6379'
|
|
||||||
restart: unless-stopped
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 4096M
|
|
||||||
reservations:
|
|
||||||
memory: 2048M
|
|
105
package.json
105
package.json
@ -6,94 +6,53 @@
|
|||||||
"start": "nx run-many --target serve",
|
"start": "nx run-many --target serve",
|
||||||
"build": "nx run-many --target build",
|
"build": "nx run-many --target build",
|
||||||
"lint": "nx run-many --target lint",
|
"lint": "nx run-many --target lint",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package"
|
||||||
"g": "nx g"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anatine/zod-nestjs": "^2.0.10",
|
"@nestjs/common": "^10.0.2",
|
||||||
"@anatine/zod-openapi": "^2.2.7",
|
"@nestjs/core": "^10.0.2",
|
||||||
"@autotelic/pino-seq-transport": "^0.1.0",
|
"@nestjs/platform-express": "^10.0.2",
|
||||||
"@fastify/send": "^4.0.0",
|
"@nx/devkit": "20.4.1",
|
||||||
"@fastify/static": "^8.1.0",
|
"axios": "^1.6.0",
|
||||||
"@keyv/redis": "^4.2.0",
|
|
||||||
"@nestjs/common": "^11.0.8",
|
|
||||||
"@nestjs/config": "^4.0.0",
|
|
||||||
"@nestjs/core": "^11.0.8",
|
|
||||||
"@nestjs/jwt": "^11.0.0",
|
|
||||||
"@nestjs/passport": "^11.0.5",
|
|
||||||
"@nestjs/platform-express": "^11.0.8",
|
|
||||||
"@nestjs/platform-fastify": "^11.0.8",
|
|
||||||
"@nestjs/schedule": "^5.0.1",
|
|
||||||
"@nestjs/swagger": "^11.0.3",
|
|
||||||
"@nestjs/terminus": "^11.0.0",
|
|
||||||
"@nestjs/typeorm": "^11.0.0",
|
|
||||||
"@nx/devkit": "20.6.0",
|
|
||||||
"@stdlib/assert": "^0.3.3",
|
|
||||||
"axios": "^1.7.9",
|
|
||||||
"bcrypt": "^5.1.1",
|
|
||||||
"cacheable": "^1.8.8",
|
|
||||||
"connection-string": "^4.4.0",
|
|
||||||
"cron": "^3.5.0",
|
|
||||||
"dayjs": "^1.11.13",
|
|
||||||
"fastify": "^5.2.1",
|
|
||||||
"flatted": "^3.3.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"nestjs-pino": "^4.3.0",
|
|
||||||
"openapi3-ts": "^4.4.0",
|
|
||||||
"passport-jwt": "^4.0.1",
|
|
||||||
"passport-local": "^1.0.0",
|
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"pino-http": "^10.4.0",
|
"reflect-metadata": "^0.1.13",
|
||||||
"pino-pretty": "^13.0.0",
|
"rxjs": "^7.8.0"
|
||||||
"reflect-metadata": "^0.2.2",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"tozod": "^3.0.0",
|
|
||||||
"typeorm": "^0.3.20",
|
|
||||||
"typeorm-naming-strategies": "^4.1.0",
|
|
||||||
"typeorm-scoped": "^1.2.0",
|
|
||||||
"typeorm-transactional": "^0.5.0",
|
|
||||||
"zod": "^3.24.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.20.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@nestjs/schematics": "^11.0.0",
|
"@nestjs/schematics": "^10.0.1",
|
||||||
"@nestjs/testing": "^11.0.8",
|
"@nestjs/testing": "^10.0.2",
|
||||||
"@nx/eslint": "20.6.0",
|
"@nx/eslint": "20.4.1",
|
||||||
"@nx/eslint-plugin": "20.6.0",
|
"@nx/eslint-plugin": "20.4.1",
|
||||||
"@nx/jest": "20.6.0",
|
"@nx/jest": "20.4.1",
|
||||||
"@nx/js": "20.6.0",
|
"@nx/js": "20.4.1",
|
||||||
"@nx/nest": "20.6.0",
|
"@nx/nest": "20.4.1",
|
||||||
"@nx/node": "20.6.0",
|
"@nx/node": "20.4.1",
|
||||||
"@nx/plugin": "20.6.0",
|
"@nx/plugin": "20.4.1",
|
||||||
"@nx/web": "20.6.0",
|
"@nx/web": "20.4.1",
|
||||||
"@nx/webpack": "20.6.0",
|
"@nx/webpack": "20.4.1",
|
||||||
"@nx/workspace": "20.6.0",
|
"@nx/workspace": "20.4.1",
|
||||||
"@swc-node/register": "~1.10.9",
|
"@swc-node/register": "~1.9.1",
|
||||||
"@swc/cli": "~0.3.12",
|
"@swc/cli": "~0.3.12",
|
||||||
"@swc/core": "~1.10.15",
|
"@swc/core": "~1.5.7",
|
||||||
"@swc/helpers": "~0.5.11",
|
"@swc/helpers": "~0.5.11",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/jest": "^29.5.14",
|
|
||||||
"@types/lodash": "^4.17.15",
|
|
||||||
"@types/node": "~18.16.9",
|
"@types/node": "~18.16.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"eslint": "^9.8.0",
|
||||||
"eslint": "^9.20.0",
|
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"execa": "5.1.1",
|
"execa": "5.1.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-node": "^29.7.0",
|
"jest-environment-node": "^29.7.0",
|
||||||
"mssql": "^11.0.1",
|
"mssql": "^11.0.1",
|
||||||
"nx": "20.6.0",
|
"nx": "20.4.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-node": "10.9.1",
|
||||||
"ts-node": "10.9.2",
|
"tslib": "^2.3.0",
|
||||||
"tslib": "^2.8.1",
|
|
||||||
"typeorm-model-generator": "0.4.6-no-engines",
|
"typeorm-model-generator": "0.4.6-no-engines",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.23.0",
|
"typescript-eslint": "^8.19.0",
|
||||||
"webpack-cli": "^5.1.4"
|
"webpack-cli": "^5.1.4"
|
||||||
},
|
}
|
||||||
"packageManager": "pnpm@9.14.1+sha512.7f1de9cffea40ff4594c48a94776112a0db325e81fb18a9400362ff7b7247f4fbd76c3011611c9f8ac58743c3dc526017894e07948de9b72052f874ee2edfdcd"
|
|
||||||
}
|
}
|
||||||
|
15339
pnpm-lock.yaml
generated
15339
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -2,5 +2,5 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class <%= className %>Service {
|
export class <%= className %>Service {
|
||||||
private readonly logger = new Logger(<%= className %>Service.name);
|
protected readonly logger = new Logger(<%= className %>Service.name);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
"generateConstructor": true,
|
"generateConstructor": true,
|
||||||
"customNamingStrategyPath": ".tomg-naming-strategy.js",
|
"customNamingStrategyPath": ".tomg-naming-strategy.js",
|
||||||
"relationIds": false,
|
"relationIds": false,
|
||||||
"strictMode": "!",
|
"strictMode": "none",
|
||||||
"skipSchema": true,
|
"skipSchema": true,
|
||||||
"indexFile": false,
|
"indexFile": false,
|
||||||
"exportType": "named",
|
"exportType": "named",
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"strict": true,
|
|
||||||
"target": "es2015",
|
"target": "es2015",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"lib": ["es2020", "dom"],
|
"lib": ["es2020", "dom"],
|
||||||
|
Reference in New Issue
Block a user