first commit

This commit is contained in:
Bastian Wagner
2026-07-31 21:02:47 +02:00
commit 6bea4f766a
512 changed files with 64459 additions and 0 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
myteamwallet_frontend/
**/node_modules
**/dist
**/.angular
**/.git
**/coverage
**/.env
**/.vscode
**/.idea

43
.env Normal file
View File

@@ -0,0 +1,43 @@
NODE_ENV=develop
APP_PORT=3999
APP_NAME="NestJS API"
API_PREFIX=api
APP_FALLBACK_LANGUAGE=en
APP_HEADER_LANGUAGE=x-custom-lang
FRONTEND_DOMAIN=http://localhost:3000
BACKEND_DOMAIN=http://localhost:3999
DATABASE_TYPE=mysql
DATABASE_HOST=localhost
DATABASE_PORT=3306
DATABASE_USERNAME=teamwallet
DATABASE_PASSWORD=SYdktg804sEpwaBNsfjW8wje70clnju7LVgwGRVn
DATABASE_NAME=teamwallet
DATABASE_SYNCHRONIZE=true
DATABASE_MAX_CONNECTIONS=100
DATABASE_SSL_ENABLED=false
DATABASE_REJECT_UNAUTHORIZED=false
DATABASE_CA=
DATABASE_KEY=
DATABASE_CERT=
# Support "local", "s3"
FILE_DRIVER=local
ACCESS_KEY_ID=
SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_DEFAULT_S3_BUCKET=
MAIL_HOST=smtp.strato.de
MAIL_PORT=465
MAIL_USER=accountservice@myteamwallet.de
MAIL_PASSWORD=HmQnnMZmw9aqwcolX5cSJjazhMEhT3Vve3v6wAxt
MAIL_IGNORE_TLS=true
MAIL_SECURE=true
MAIL_REQUIRE_TLS=false
MAIL_DEFAULT_EMAIL=accountservice@myteamwallet.de
MAIL_DEFAULT_NAME=accountservice@myteamwallet.de
MAIL_CLIENT_PORT=1080
AUTH_JWT_SECRET=this-is-onE-longer-secret
AUTH_JWT_TOKEN_EXPIRES_IN=100d

57
.env.example Normal file
View File

@@ -0,0 +1,57 @@
NODE_ENV=production
APP_PORT=3999
APP_NAME="NestJS API"
API_PREFIX=api
APP_FALLBACK_LANGUAGE=en
APP_HEADER_LANGUAGE=x-custom-lang
FRONTEND_DOMAIN=http://localhost:3999
BACKEND_DOMAIN=http://localhost:3999
# External database - not part of this container
DATABASE_TYPE=mysql
DATABASE_HOST=change-me
DATABASE_PORT=3306
DATABASE_USERNAME=change-me
DATABASE_PASSWORD=change-me
DATABASE_NAME=change-me
DATABASE_SYNCHRONIZE=false
DATABASE_MAX_CONNECTIONS=100
DATABASE_SSL_ENABLED=false
DATABASE_REJECT_UNAUTHORIZED=false
DATABASE_CA=
DATABASE_KEY=
DATABASE_CERT=
# Support "local", "s3"
FILE_DRIVER=local
ACCESS_KEY_ID=
SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_DEFAULT_S3_BUCKET=
MAIL_HOST=change-me
MAIL_PORT=465
MAIL_USER=change-me
MAIL_PASSWORD=change-me
MAIL_IGNORE_TLS=true
MAIL_SECURE=true
MAIL_REQUIRE_TLS=false
MAIL_DEFAULT_EMAIL=noreply@example.com
MAIL_DEFAULT_NAME=Api
MAIL_CLIENT_PORT=1080
AUTH_JWT_SECRET=change-me
AUTH_JWT_TOKEN_EXPIRES_IN=100d
FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
APPLE_APP_AUDIENCE=[]
TWITTER_CONSUMER_KEY=
TWITTER_CONSUMER_SECRET=
WORKER_HOST=redis://redis:6379/1

40
Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
## Build the new Angular frontend (same-origin /api/v1/ base URL)
FROM node:22-alpine AS frontend-build
WORKDIR /app/frontend
COPY myteamwallet_frontend_modern/package.json myteamwallet_frontend_modern/package-lock.json ./
RUN npm ci
COPY myteamwallet_frontend_modern/ ./
RUN npm run build:container
## Build the NestJS backend
FROM node:22-alpine AS backend-build
WORKDIR /app
ENV CI=true
COPY myteamwallet_backend/package.json myteamwallet_backend/package-lock.json ./
RUN npm ci
COPY myteamwallet_backend/ ./
RUN npm run build
## Runtime image: backend + frontend static assets, external DB only
FROM node:22-alpine
WORKDIR /app
ENV CI=true
RUN apk add --no-cache bash
COPY myteamwallet_backend/package.json myteamwallet_backend/package-lock.json ./
RUN npm ci
ENV NODE_ENV=production
COPY --from=backend-build /app/dist ./dist
COPY myteamwallet_backend/src ./src
COPY myteamwallet_backend/tsconfig.json ./tsconfig.json
COPY myteamwallet_backend/wait-for-it.sh ./wait-for-it.sh
COPY myteamwallet_backend/docker-entrypoint.sh ./docker-entrypoint.sh
RUN sed -i 's/\r$//' wait-for-it.sh docker-entrypoint.sh \
&& chmod +x wait-for-it.sh docker-entrypoint.sh
COPY --from=frontend-build /app/frontend/dist/myteamwallet_frontend_modern/browser ./client
EXPOSE 3999
ENTRYPOINT ["./docker-entrypoint.sh"]

43
env-example Normal file
View File

@@ -0,0 +1,43 @@
NODE_ENV=development
APP_PORT=3000
APP_NAME="NestJS API"
API_PREFIX=api
APP_FALLBACK_LANGUAGE=en
APP_HEADER_LANGUAGE=x-custom-lang
FRONTEND_DOMAIN=http://localhost:3000
BACKEND_DOMAIN=http://localhost:3000
DATABASE_TYPE=postgres
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_USERNAME=root
DATABASE_PASSWORD=secret
DATABASE_NAME=api
DATABASE_SYNCHRONIZE=false
DATABASE_MAX_CONNECTIONS=100
DATABASE_SSL_ENABLED=false
DATABASE_REJECT_UNAUTHORIZED=false
DATABASE_CA=
DATABASE_KEY=
DATABASE_CERT=
# Support "local", "s3"
FILE_DRIVER=local
ACCESS_KEY_ID=
SECRET_ACCESS_KEY=
AWS_S3_REGION=
AWS_DEFAULT_S3_BUCKET=
MAIL_HOST=maildev
MAIL_PORT=1025
MAIL_USER=
MAIL_PASSWORD=
MAIL_IGNORE_TLS=true
MAIL_SECURE=false
MAIL_REQUIRE_TLS=false
MAIL_DEFAULT_EMAIL=noreply@example.com
MAIL_DEFAULT_NAME=Api
MAIL_CLIENT_PORT=1080
AUTH_JWT_SECRET=secret
AUTH_JWT_TOKEN_EXPIRES_IN=1d

View File

@@ -0,0 +1,30 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
'require-await': 'off',
'@typescript-eslint/require-await': 'error',
'@typescript-eslint/no-floating-promises': 'error',
},
};

41
myteamwallet_backend/.gitignore vendored Normal file
View File

@@ -0,0 +1,41 @@
# compiled output
/dist
/client
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.data
/files
.env
/ormconfig.json

View File

@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint

View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -0,0 +1,7 @@
{
"conventionalCommits.scopes": [
"files",
"deploy",
"auth"
]
}

View File

@@ -0,0 +1,2 @@
web: npm run start:prod
release: echo '' > .env && npm run migration:run && npm run seed:run

View File

@@ -0,0 +1,141 @@
# PROD System
Datenbank auf Server: teamwallet
# NestJS REST API boilerplate 🇺🇦
`ssh -L 3306:127.0.0.1:3306 bastian@h2979752.stratoserver.net`
## Description
Seeden: npm run seed:run
## Table of Contents
- [Features](#features)
- [Quick run](#quick-run)
- [Comfortable development](#comfortable-development)
- [Links](#links)
- [Automatic update of dependencies](#automatic-update-of-dependencies)
- [Database utils](#database-utils)
- [Tests](#tests)
## Features
- [x] Database ([typeorm](https://www.npmjs.com/package/typeorm)).
- [x] Seeding.
- [x] Config Service ([@nestjs/config](https://www.npmjs.com/package/@nestjs/config)).
- [x] Mailing ([nodemailer](https://www.npmjs.com/package/nodemailer), [@nestjs-modules/mailer](https://www.npmjs.com/package/@nestjs-modules/mailer)).
- [x] Sign in and sign up via email.
- [x] Social sign in (Apple, Facebook, Google, Twitter).
- [x] Admin and User roles.
- [x] I18N ([nestjs-i18n](https://www.npmjs.com/package/nestjs-i18n)).
- [x] File uploads. Support local and Amazon S3 drivers.
- [x] Swagger.
- [x] E2E and units tests.
- [x] Docker.
- [x] CI (Github Actions).
## Quick run
```bash
git clone --depth 1 https://github.com/brocoders/nestjs-boilerplate.git my-app
cd my-app/
cp env-example .env
docker compose up -d
```
For check status run
```bash
docker compose logs
```
## Comfortable development
```bash
git clone --depth 1 https://github.com/brocoders/nestjs-boilerplate.git my-app
cd my-app/
cp env-example .env
```
Change `DATABASE_HOST=postgres` to `DATABASE_HOST=localhost`
Change `MAIL_HOST=maildev` to `MAIL_HOST=localhost`
Run additional container:
```bash
docker compose up -d postgres adminer maildev redis
```
```bash
npm install
npm run migration:run
npm run seed:run
npm run start:dev
```
## Links
- Swagger: http://localhost:3000/docs
- Adminer (client for DB): http://localhost:8080
- Maildev: http://localhost:1080
## Automatic update of dependencies
If you want to automatically update dependencies, you can connect [Renovate](https://github.com/marketplace/renovate) for your project.
## Database utils
Generate migration
```bash
npm run migration:generate -- src/database/migrations/CreateNameTable
```
Run migration
```bash
npm run migration:run
```
Revert migration
```bash
npm run migration:revert
```
Drop all tables in database
```bash
npm run schema:drop
```
Run seed
```bash
npm run seed:run
```
## Tests
```bash
# unit tests
npm run test
# e2e tests
npm run test:e2e
```
## Tests in Docker
```bash
docker compose -f docker-compose.ci.yaml --env-file env-example -p ci up --build --exit-code-from api && docker compose -p ci rm -svf
```
## Test benchmarking
```bash
docker run --rm jordi/ab -n 100 -c 100 -T application/json -H "Authorization: Bearer USER_TOKEN" -v 2 http://<server_ip>:3000/api/v1/users
```

View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -e
echo "Waiting for database at ${DATABASE_HOST}:${DATABASE_PORT}..."
bash ./wait-for-it.sh "${DATABASE_HOST}:${DATABASE_PORT}" -t 60
echo "Running database migrations..."
# env-cmd (used by migration:run) requires an .env file to exist, even though
# the real config already arrived as process env vars via `docker run --env-file`.
touch .env
npm run migration:run
echo "Starting application..."
exec node dist/main

View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [{ "include": "i18n/**/*", "watchAssets": true }]
}
}

32362
myteamwallet_backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
{
"name": "nestjs-boilerplate",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"postinstall": "is-ci || husky install",
"typeorm": "env-cmd ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js",
"migration:generate": "npm run typeorm -- --dataSource=src/database/data-source.ts migration:generate",
"migration:create": "npm run typeorm -- migration:create",
"migration:run": "npm run typeorm -- --dataSource=src/database/data-source.ts migration:run",
"migration:revert": "npm run typeorm -- --dataSource=src/database/data-source.ts migration:revert",
"schema:drop": "npm run typeorm -- --dataSource=src/database/data-source.ts schema:drop",
"seed:run": "ts-node -r tsconfig-paths/register ./src/database/seeds/run-seed.ts",
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "env-cmd jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs-modules/mailer": "1.8.1",
"@nestjs/common": "9.1.6",
"@nestjs/config": "2.2.0",
"@nestjs/core": "9.1.6",
"@nestjs/jwt": "9.0.0",
"@nestjs/passport": "9.0.0",
"@nestjs/platform-express": "9.1.6",
"@nestjs/serve-static": "^3.0.0",
"@nestjs/swagger": "6.1.3",
"@nestjs/typeorm": "9.0.1",
"apple-signin-auth": "1.7.4",
"bcryptjs": "2.4.3",
"class-transformer": "0.5.1",
"class-validator": "0.13.2",
"fb": "2.0.0",
"google-auth-library": "8.7.0",
"handlebars": "4.7.7",
"multer": "1.4.4",
"multer-s3": "2.10.0",
"mysql2": "^2.3.3",
"nestjs-i18n": "9.2.2",
"nodemailer": "6.8.0",
"passport": "0.6.0",
"passport-anonymous": "1.0.1",
"passport-jwt": "4.0.0",
"pg": "8.8.0",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "7.5.7",
"source-map-support": "0.5.21",
"swagger-ui-express": "4.5.0",
"twitter": "1.7.1",
"typeorm": "0.3.10"
},
"devDependencies": {
"@faker-js/faker": "^7.6.0",
"@nestjs/cli": "9.1.5",
"@nestjs/schematics": "9.0.3",
"@nestjs/testing": "9.1.6",
"@types/bcryptjs": "2.4.2",
"@types/express": "4.17.14",
"@types/facebook-js-sdk": "3.3.6",
"@types/jest": "29.2.3",
"@types/multer": "1.4.7",
"@types/node": "16.18.3",
"@types/passport-anonymous": "1.0.3",
"@types/passport-jwt": "3.0.7",
"@types/supertest": "2.0.12",
"@types/twitter": "1.7.1",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/parser": "5.43.0",
"aws-sdk": "2.1243.0",
"env-cmd": "10.1.0",
"eslint": "8.27.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-prettier": "4.2.1",
"husky": "7.0.4",
"is-ci": "3.0.1",
"jest": "29.3.1",
"prettier": "2.7.1",
"supertest": "6.3.1",
"ts-jest": "29.0.1",
"ts-loader": "9.4.1",
"ts-node": "10.9.1",
"tsconfig-paths": "4.1.0",
"tslib": "2.4.1",
"typescript": "4.8.4"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/$1"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"engines": {
"node": ">=14.0.0"
}
}

View File

@@ -0,0 +1,5 @@
{
"extends": [
"config:base"
]
}

View File

@@ -0,0 +1,86 @@
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';
import { FilesModule } from './files/files.module';
import { AuthModule } from './auth/auth.module';
import databaseConfig from './config/database.config';
import authConfig from './config/auth.config';
import appConfig from './config/app.config';
import mailConfig from './config/mail.config';
import fileConfig from './config/file.config';
import * as path from 'path';
import { MailerModule } from '@nestjs-modules/mailer';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { I18nModule } from 'nestjs-i18n/dist/i18n.module';
import { HeaderResolver } from 'nestjs-i18n';
import { TypeOrmConfigService } from './database/typeorm-config.service';
import { MailConfigService } from './mail/mail-config.service';
import { ForgotModule } from './forgot/forgot.module';
import { MailModule } from './mail/mail.module';
import { DataSource } from 'typeorm';
import { PlayersModule } from './players/players.module';
import { TeamsModule } from './teams/teams.module';
import { TransactionsModule } from './transactions/transactions.module';
import { TeamSettingsModule } from './team-settings/team-settings.module';
import { TeamWalletTransactionsModule } from './team-wallet-transactions/team-wallet-transactions.module';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { LoggingModule } from './database/logging/logging.module';
import { TranslateModule } from './translate/translate.module';
import { PenaltyModule } from './penalty/penalty.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, authConfig, appConfig, mailConfig, fileConfig],
envFilePath: ['.env'],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
dataSourceFactory: async (options) => {
const dataSource = await new DataSource(options).initialize();
return dataSource;
},
}),
MailerModule.forRootAsync({
useClass: MailConfigService,
}),
I18nModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
fallbackLanguage: configService.get('app.fallbackLanguage'),
loaderOptions: { path: path.join(__dirname, '/i18n/'), watch: true },
}),
resolvers: [
{
use: HeaderResolver,
useFactory: (configService: ConfigService) => {
return [configService.get('app.headerLanguage')];
},
inject: [ConfigService],
},
],
imports: [ConfigModule],
inject: [ConfigService],
}),
ServeStaticModule.forRoot({
rootPath: join(__dirname, '../client'),
exclude: ['*/api*'],
}),
UsersModule,
FilesModule,
AuthModule,
ForgotModule,
MailModule,
PlayersModule,
TeamsModule,
TransactionsModule,
TeamSettingsModule,
TeamWalletTransactionsModule,
LoggingModule,
TranslateModule,
PenaltyModule,
],
providers: [],
})
export class AppModule {}

View File

@@ -0,0 +1,7 @@
export enum AuthProvidersEnum {
email = 'email',
facebook = 'facebook',
google = 'google',
twitter = 'twitter',
apple = 'apple',
}

View File

@@ -0,0 +1,139 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Request,
Post,
UseGuards,
Patch,
Delete,
UseInterceptors,
ClassSerializerInterceptor,
SerializeOptions,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthEmailLoginDto } from './dto/auth-email-login.dto';
import { AuthForgotPasswordDto } from './dto/auth-forgot-password.dto';
import { AuthConfirmEmailDto } from './dto/auth-confirm-email.dto';
import { AuthResetPasswordDto } from './dto/auth-reset-password.dto';
import { AuthUpdateDto } from './dto/auth-update.dto';
import { AuthGuard } from '@nestjs/passport';
import {
ApiTags,
ApiBearerAuth,
ApiOperation,
ApiOkResponse,
} from '@nestjs/swagger';
import { CreateInviteDTO } from './dto/create-invite.dto';
@ApiTags('Auth')
@Controller({
path: 'auth',
version: '1',
})
@UseInterceptors(ClassSerializerInterceptor)
export class AuthController {
constructor(public service: AuthService) {}
@Post('email/login')
@HttpCode(HttpStatus.OK)
public async login(@Body() loginDto: AuthEmailLoginDto) {
return this.service.validateLogin(loginDto);
}
@Post('admin/email/login')
@HttpCode(HttpStatus.OK)
public async adminLogin(@Body() loginDTO: AuthEmailLoginDto) {
return this.service.validateLogin(loginDTO);
}
@Post('email/register')
@HttpCode(HttpStatus.CREATED)
async register(@Body() createUserDto: any) {
return this.service.register(createUserDto);
}
@Post('email/confirm')
@HttpCode(HttpStatus.OK)
async confirmEmail(@Body() confirmEmailDto: AuthConfirmEmailDto) {
return this.service.confirmEmail(confirmEmailDto.hash);
}
@Post('forgot/password')
@HttpCode(HttpStatus.OK)
async forgotPassword(@Body() forgotPasswordDto: AuthForgotPasswordDto) {
return this.service.forgotPassword(forgotPasswordDto.email);
}
@Post('reset/password')
@HttpCode(HttpStatus.OK)
async resetPassword(@Body() resetPasswordDto: AuthResetPasswordDto) {
return this.service.resetPassword(
resetPasswordDto.hash,
resetPasswordDto.password,
);
}
@ApiBearerAuth()
@SerializeOptions({
groups: ['exposeProvider'],
})
@Get('me')
// @UseGuards(AuthGuard('jwt'))
@HttpCode(HttpStatus.OK)
public me(@Request() request: Request) {
return this.service.me(request.headers['authorization']);
}
@ApiBearerAuth()
@Patch('me')
@UseGuards(AuthGuard('jwt'))
@HttpCode(HttpStatus.OK)
public async update(@Request() request, @Body() userDto: AuthUpdateDto) {
return this.service.update(request.user, userDto);
}
@ApiBearerAuth()
@Delete('me')
@UseGuards(AuthGuard('jwt'))
@HttpCode(HttpStatus.OK)
public async delete(@Request() request) {
return this.service.softDelete(request.user);
}
@ApiOperation({
summary: 'Erstellt Registrierungstoken',
description:
'Encoded den JWT Token für eine Einladung für eine neue Registrierung. Team- und Rollen-Infos müssen da sein. Der Token ist dann 30 Tage gültig.',
})
@ApiOkResponse({
description: 'JWT Token',
isArray: false,
type: 'string',
})
@Post('invite')
@UseGuards(AuthGuard('jwt'))
public getInvite(
@Body()
invite: any,
) {
return this.service.createTeamInvite(invite);
}
@ApiOperation({
summary: 'Verifiziert Registrierungstoken',
description:
'Decoded und prüft den JWT Token einer Einladung für die Registrierung',
})
@ApiOkResponse({
description: 'Object mit teamName, teamId, roleName, roleId',
isArray: false,
type: CreateInviteDTO,
})
@Post('verify-invite')
public verifyInvite(@Body() body: any) {
return this.service.getTeamFromInvite(body.token);
}
}

View File

@@ -0,0 +1,38 @@
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from './strategies/jwt.strategy';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AnonymousStrategy } from './strategies/anonymous.strategy';
import { UsersModule } from 'src/users/users.module';
import { ForgotModule } from 'src/forgot/forgot.module';
import { MailModule } from 'src/mail/mail.module';
import { IsExist } from 'src/utils/validators/is-exists.validator';
import { IsNotExist } from 'src/utils/validators/is-not-exists.validator';
import { LoggingModule } from 'src/database/logging/logging.module';
@Module({
imports: [
UsersModule,
ForgotModule,
PassportModule,
MailModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get('auth.secret'),
signOptions: {
expiresIn: configService.get('auth.expires'),
},
}),
}),
LoggingModule,
],
controllers: [AuthController],
providers: [IsExist, IsNotExist, AuthService, JwtStrategy, AnonymousStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,405 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { User } from '../users/entities/user.entity';
import * as bcrypt from 'bcryptjs';
import { AuthEmailLoginDto } from './dto/auth-email-login.dto';
import { AuthUpdateDto } from './dto/auth-update.dto';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import { RoleEnum } from 'src/roles/roles.enum';
import { StatusEnum } from 'src/statuses/statuses.enum';
import * as crypto from 'crypto';
import { plainToClass } from 'class-transformer';
import { Status } from 'src/statuses/entities/status.entity';
import { Role } from 'src/roles/entities/role.entity';
import { AuthProvidersEnum } from './auth-providers.enum';
import { SocialInterface } from 'src/social/interfaces/social.interface';
import { AuthRegisterLoginDto } from './dto/auth-register-login.dto';
import { UsersService } from 'src/users/users.service';
import { ForgotService } from 'src/forgot/forgot.service';
import { MailService } from 'src/mail/mail.service';
import { CreateInviteDTO } from './dto/create-invite.dto';
import { LoggingService } from 'src/database/logging/logging.service';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private usersService: UsersService,
private forgotService: ForgotService,
private mailService: MailService,
private logger: LoggingService,
) {}
async validateLogin(
loginDto: AuthEmailLoginDto,
): Promise<{ token: string; user: User }> {
const user = await this.usersService.findOne({
email: loginDto.email,
});
if (!user) {
await this.logger.info({
event: 'user_login_fail',
details: `mail not found: ${loginDto.email}`,
userId: -1,
});
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
email: 'notFound',
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
if (user.provider !== AuthProvidersEnum.email) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
email: `needLoginViaProvider:${user.provider}`,
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
const isValidPassword = await bcrypt.compare(
loginDto.password,
user.password,
);
if (isValidPassword) {
const token = await this.jwtService.sign({
id: user.id,
role: user.role,
});
await this.logger.info({
event: 'user_login_success',
details: `logged in: ${loginDto.email}`,
userId: user.id,
});
return { token, user: user };
} else {
await this.logger.info({
event: 'user_login_fail',
details: `incorrect password for user: ${loginDto.email}`,
userId: user.id,
});
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
password: 'incorrectPassword',
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
}
async validateSocialLogin(
authProvider: string,
socialData: SocialInterface,
): Promise<{ token: string; user: User }> {
let user: User;
const socialEmail = socialData.email?.toLowerCase();
const userByEmail = await this.usersService.findOne({
email: socialEmail,
});
user = await this.usersService.findOne({
socialId: socialData.id,
provider: authProvider,
});
if (user) {
if (socialEmail && !userByEmail) {
user.email = socialEmail;
}
await this.usersService.update(user.id, user);
} else if (userByEmail) {
user = userByEmail;
} else {
const role = plainToClass(Role, {
id: RoleEnum.user,
});
const status = plainToClass(Status, {
id: StatusEnum.active,
});
user = await this.usersService.create({
email: socialEmail,
firstName: socialData.firstName,
lastName: socialData.lastName,
socialId: socialData.id,
provider: authProvider,
role,
status,
});
user = await this.usersService.findOne({
id: user.id,
});
}
const jwtToken = await this.jwtService.sign({
id: user.id,
role: user.role,
});
return {
token: jwtToken,
user,
};
}
async register(dto: AuthRegisterLoginDto): Promise<void> {
const hash = crypto
.createHash('sha256')
.update(randomStringGenerator())
.digest('hex');
const user = await this.usersService.create({
...dto,
email: dto.email,
role: {
id: RoleEnum.user,
} as Role,
status: {
id: StatusEnum.inactive,
} as Status,
hash,
});
if (user && dto.linkPlayerId != null) {
await this.usersService.linkPlayerToUserId(user, dto.linkPlayerId);
}
await this.logger.info({
event: 'user_create',
details: `user created with mail: ${dto.email}`,
userId: user.id,
});
await this.mailService.userSignUp({
to: user.email,
data: {
hash,
},
});
}
async confirmEmail(hash: string): Promise<void> {
const user = await this.usersService.findOne({
hash,
});
if (!user) {
throw new HttpException(
{
status: HttpStatus.NOT_FOUND,
error: `notFound`,
},
HttpStatus.NOT_FOUND,
);
}
user.hash = null;
user.status = plainToClass(Status, {
id: StatusEnum.active,
});
await user.save();
}
async forgotPassword(email: string): Promise<void> {
const user = await this.usersService.findOne({
email,
});
if (!user) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
email: 'emailNotExists',
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
} else {
const hash = crypto
.createHash('sha256')
.update(randomStringGenerator())
.digest('hex');
await this.forgotService.create({
hash,
user,
});
await this.mailService.forgotPassword({
to: email,
data: {
hash,
},
});
}
}
async resetPassword(hash: string, password: string): Promise<void> {
const forgot = await this.forgotService.findOne({
where: {
hash,
},
});
if (!forgot) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
hash: `notFound`,
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
const user = forgot.user;
user.password = password;
await user.save();
await this.forgotService.softDelete(forgot.id);
}
async me(token: string): Promise<User> {
token = token.replace('Bearer ', '');
let role: any;
try {
role = this.jwtService.verify(token);
const u = await this.usersService.findOne({
id: role.id,
});
await this.logger.debug({
event: 'user_token_verification_success',
details: `Email: ${u.email}`,
userId: u.id,
});
return u;
} catch (error) {
const role = this.jwtService.decode(token);
const user = await this.usersService.findOne({
id: (role as any).id,
});
const t = await this.jwtService.sign({
id: user.id,
role: user.role,
});
user['token'] = t;
await this.logger.debug({
event: 'user_token_verification_success',
details: `Email: ${user.email}`,
userId: user.id,
});
return user;
}
}
async update(user: User, userDto: AuthUpdateDto): Promise<User> {
if (userDto.password) {
if (userDto.oldPassword) {
const currentUser = await this.usersService.findOne({
id: user.id,
});
const isValidOldPassword = await bcrypt.compare(
userDto.oldPassword,
currentUser.password,
);
if (!isValidOldPassword) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
oldPassword: 'incorrectOldPassword',
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
} else {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
oldPassword: 'missingOldPassword',
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
}
await this.usersService.update(user.id, userDto);
return this.usersService.findOne({
id: user.id,
});
}
async softDelete(user: User): Promise<void> {
await this.usersService.softDelete(user.id);
}
async createTeamInvite(object: CreateInviteDTO) {
const token = await this.jwtService.sign(object, {
expiresIn: '30d',
});
await this.logger.info({
event: 'user_invite_link_create',
details: `invitation created for team: ${object.teamName} , ${object.teamId}`,
userId: 0,
});
return { token };
}
async getTeamFromInvite(token: string) {
try {
const teamInfo = this.jwtService.verify(token);
delete teamInfo.iat;
delete teamInfo.exp;
await this.logger.info({
event: 'user_invite_link_validate',
details: `invitation validated for team: ${teamInfo.teamName} , ${teamInfo.teamId}`,
userId: 0,
});
return teamInfo;
} catch {
await this.logger.info({
event: 'user_invite_link_validate_fail',
details: `validation failed for token ${token}`,
userId: 0,
});
throw new HttpException(
'Token not valid',
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
}
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class AuthConfirmEmailDto {
@ApiProperty()
@IsNotEmpty()
hash: string;
}

View File

@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, Validate } from 'class-validator';
import { IsExist } from 'src/utils/validators/is-exists.validator';
import { Transform } from 'class-transformer';
export class AuthEmailLoginDto {
@ApiProperty({ example: 'test1@example.com' })
@Transform(({ value }) => value.toLowerCase().trim())
@Validate(IsExist, ['User'], {
message: 'emailNotExists',
})
email: string;
@ApiProperty()
@IsNotEmpty()
password: string;
}

View File

@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { Transform } from 'class-transformer';
export class AuthForgotPasswordDto {
@ApiProperty()
@Transform(({ value }) => value.toLowerCase().trim())
@IsEmail()
email: string;
}

View File

@@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, MinLength, Validate } from 'class-validator';
import { IsNotExist } from 'src/utils/validators/is-not-exists.validator';
import { Transform } from 'class-transformer';
export class AuthRegisterLoginDto {
@ApiProperty({ example: 'test1@example.com' })
@Transform(({ value }) => value.toLowerCase().trim())
@Validate(IsNotExist, ['User'], {
message: 'emailAlreadyExists',
})
@IsEmail()
email: string;
@ApiProperty()
@MinLength(6)
password: string;
@ApiProperty({ example: 'John' })
@IsNotEmpty()
firstName: string;
@ApiProperty({ example: 'Doe' })
@IsNotEmpty()
lastName: string;
@ApiProperty({ example: 27 })
linkPlayerId: number | null;
}

View File

@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class AuthResetPasswordDto {
@ApiProperty()
@IsNotEmpty()
password: string;
@ApiProperty()
@IsNotEmpty()
hash: string;
}

View File

@@ -0,0 +1,22 @@
import { ApiProperty } from '@nestjs/swagger';
import { Allow, IsNotEmpty } from 'class-validator';
import { Tokens } from 'src/social/tokens';
import { AuthProvidersEnum } from '../auth-providers.enum';
export class AuthSocialLoginDto {
@Allow()
@ApiProperty({ type: () => Tokens })
tokens: Tokens;
@ApiProperty({ enum: AuthProvidersEnum })
@IsNotEmpty()
socialType: AuthProvidersEnum;
@Allow()
@ApiProperty({ required: false })
firstName?: string;
@Allow()
@ApiProperty({ required: false })
lastName?: string;
}

View File

@@ -0,0 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, MinLength, Validate } from 'class-validator';
import { IsExist } from '../../utils/validators/is-exists.validator';
import { FileEntity } from '../../files/entities/file.entity';
export class AuthUpdateDto {
@ApiProperty({ type: () => FileEntity })
@IsOptional()
@Validate(IsExist, ['FileEntity', 'id'], {
message: 'imageNotExists',
})
photo?: FileEntity;
@ApiProperty({ example: 'John' })
@IsOptional()
@IsNotEmpty({ message: 'mustBeNotEmpty' })
firstName?: string;
@ApiProperty({ example: 'Doe' })
@IsOptional()
@IsNotEmpty({ message: 'mustBeNotEmpty' })
lastName?: string;
@ApiProperty()
@IsOptional()
@IsNotEmpty()
@MinLength(6)
password?: string;
@ApiProperty()
@IsOptional()
@IsNotEmpty({ message: 'mustBeNotEmpty' })
oldPassword: string;
}

View File

@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
export class CreateInviteDTO {
@ApiProperty()
teamId: number;
@ApiProperty({ example: 'Development Team' })
teamName: string;
@ApiProperty()
playerId: number;
@ApiProperty({ example: 'Max Mustermann' })
playerName: string;
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class VerifyTokenDTO {
@ApiProperty({ example: 'JWT' })
token: string;
}

View File

@@ -0,0 +1,14 @@
import { Strategy } from 'passport-anonymous';
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
@Injectable()
export class AnonymousStrategy extends PassportStrategy(Strategy) {
constructor() {
super();
}
public validate(payload: unknown, request: unknown): unknown {
return request;
}
}

View File

@@ -0,0 +1,28 @@
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PassportStrategy } from '@nestjs/passport';
import { User } from '../../users/entities/user.entity';
import { ConfigService } from '@nestjs/config';
type JwtPayload = Pick<User, 'id' | 'role'> & { iat: number; exp: number };
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get('auth.secret'),
});
}
public validate(payload: JwtPayload) {
if (!payload.id) {
throw new UnauthorizedException();
}
return payload;
}
}

View File

@@ -0,0 +1,13 @@
import { registerAs } from '@nestjs/config';
export default registerAs('app', () => ({
nodeEnv: process.env.NODE_ENV,
name: process.env.APP_NAME,
workingDirectory: process.env.PWD || process.cwd(),
frontendDomain: process.env.FRONTEND_DOMAIN,
backendDomain: process.env.BACKEND_DOMAIN,
port: parseInt(process.env.APP_PORT || process.env.PORT, 10) || 3000,
apiPrefix: process.env.API_PREFIX || 'api',
fallbackLanguage: process.env.APP_FALLBACK_LANGUAGE || 'en',
headerLanguage: process.env.APP_HEADER_LANGUAGE || 'x-custom-lang',
}));

View File

@@ -0,0 +1,6 @@
import { registerAs } from '@nestjs/config';
export default registerAs('auth', () => ({
secret: process.env.AUTH_JWT_SECRET,
expires: process.env.AUTH_JWT_TOKEN_EXPIRES_IN,
}));

View File

@@ -0,0 +1,17 @@
import { registerAs } from '@nestjs/config';
export default registerAs('database', () => ({
url: process.env.DATABASE_URL,
type: process.env.DATABASE_TYPE,
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
password: process.env.DATABASE_PASSWORD,
name: process.env.DATABASE_NAME,
username: process.env.DATABASE_USERNAME,
synchronize: process.env.DATABASE_SYNCHRONIZE === 'true',
sslEnabled: process.env.DATABASE_SSL_ENABLED === 'true',
rejectUnauthorized: process.env.DATABASE_REJECT_UNAUTHORIZED === 'true',
ca: process.env.DATABASE_CA,
key: process.env.DATABASE_KEY,
cert: process.env.DATABASE_CERT,
}));

View File

@@ -0,0 +1,11 @@
import { registerAs } from '@nestjs/config';
export default registerAs('file', () => ({
driver: process.env.FILE_DRIVER,
accessKeyId: process.env.ACCESS_KEY_ID,
secretAccessKey: process.env.SECRET_ACCESS_KEY,
awsDefaultS3Bucket: process.env.AWS_DEFAULT_S3_BUCKET,
awsDefaultS3Url: process.env.AWS_DEFAULT_S3_URL,
awsS3Region: process.env.AWS_S3_REGION,
maxFileSize: 5242880, // 5mb
}));

View File

@@ -0,0 +1,13 @@
import { registerAs } from '@nestjs/config';
export default registerAs('mail', () => ({
port: parseInt(process.env.MAIL_PORT, 10),
host: process.env.MAIL_HOST,
user: process.env.MAIL_USER,
password: process.env.MAIL_PASSWORD,
defaultEmail: process.env.MAIL_DEFAULT_EMAIL,
defaultName: process.env.MAIL_DEFAULT_NAME,
ignoreTLS: process.env.MAIL_IGNORE_TLS === 'true',
secure: process.env.MAIL_SECURE === 'true',
requireTLS: process.env.MAIL_REQUIRE_TLS === 'true',
}));

View File

@@ -0,0 +1,23 @@
import 'reflect-metadata';
import { DataSource, DataSourceOptions } from 'typeorm';
export const AppDataSource = new DataSource({
type: process.env.DATABASE_TYPE,
url: process.env.DATABASE_URL,
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
synchronize: process.env.DATABASE_SYNCHRONIZE === 'true',
dropSchema: false,
keepConnectionAlive: true,
logging: process.env.NODE_ENV !== 'production',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
cli: {
entitiesDir: 'src',
migrationsDir: 'src/database/migrations',
subscribersDir: 'subscriber',
},
} as DataSourceOptions);

View File

@@ -0,0 +1,13 @@
import { LOGEVENT, LOGLEVEL } from '../model/logging-event.type';
export class CreateLogDTO {
level: LOGLEVEL;
event: LOGEVENT;
details: string;
userId: number;
duration?: number;
}

View File

@@ -0,0 +1,39 @@
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { EntityHelper } from 'src/utils/entity-helper';
import { LOGEVENT, LOGLEVEL } from '../model/logging-event.type';
@Entity()
export class LogEntry extends EntityHelper {
@ApiProperty({ example: 1 })
@PrimaryGeneratedColumn()
id: number;
@ApiProperty({ example: 'error' })
@Column()
level: LOGLEVEL;
@ApiProperty({ example: 'created new user' })
@Column()
event: LOGEVENT;
@ApiProperty({ example: 'created new user max mustermann' })
@Column()
details: string;
@ApiProperty({ example: 5 })
@Column()
userId: number;
@ApiProperty({ example: 5 })
@Column({ nullable: true })
duration?: number;
@CreateDateColumn()
createdAt: Date;
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { LogEntry } from './entities/log-entry.entity';
import { LoggingService } from './logging.service';
@Module({
imports: [TypeOrmModule.forFeature([LogEntry])],
providers: [LoggingService],
exports: [LoggingService],
})
export class LoggingModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { LoggingService } from './logging.service';
describe('LoggingService', () => {
let service: LoggingService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [LoggingService],
}).compile();
service = module.get<LoggingService>(LoggingService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,95 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateLogDTO } from './dto/create-log.dto';
import { LogEntry } from './entities/log-entry.entity';
import { LOGEVENT } from './model/logging-event.type';
@Injectable()
export class LoggingService {
constructor(
@InjectRepository(LogEntry)
private repository: Repository<LogEntry>,
) {
// void this.onStart();
}
private async onStart() {
await this.repository.save({
level: 'DEBUG',
event: 'application_start',
details: 'application started',
userId: -1,
});
}
async info({
event,
details,
userId,
}: {
event: LOGEVENT;
details: string;
userId: number;
}) {
const e: CreateLogDTO = {
event,
details,
userId,
level: 'INFO',
};
await this.repository.save(e);
}
async error({
event,
details,
userId,
}: {
event: LOGEVENT;
details: string;
userId: number;
}) {
const e: CreateLogDTO = {
event,
details,
userId,
level: 'ERROR',
};
await this.repository.save(e);
}
async warn({
event,
details,
userId,
}: {
event: LOGEVENT;
details: string;
userId: number;
}) {
const e: CreateLogDTO = {
event,
details,
userId,
level: 'WARN',
};
await this.repository.save(e);
}
async debug(data: {
event: LOGEVENT;
details: string;
userId: number;
duration?: number;
}) {
const e: CreateLogDTO = {
event: data.event,
details: data.details,
userId: data.userId,
level: 'DEBUG',
duration: data.duration,
};
await this.repository.save(e);
}
}

View File

@@ -0,0 +1,5 @@
export enum LogTypeEnum {
'error' = 1,
'info' = 2,
'warn' = 3,
}

View File

@@ -0,0 +1,20 @@
export type LOGEVENT =
| 'user_create'
| 'application_start'
| 'transaction_create'
| 'team_transaction_create'
| 'team_transaction_get'
| 'user_login_success'
| 'user_login_fail'
| 'user_token_verification_success'
| 'user_token_verification_fail'
| 'user_invite_link_create'
| 'user_invite_link_validate'
| 'user_invite_link_validate_fail'
| 'transaction_create'
| 'transaction_create_fail'
| 'transaction_reverse'
| 'player_creation'
| 'team_create';
export type LOGLEVEL = 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'TRACE';

View File

@@ -0,0 +1,75 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUser1604164774154 implements MigrationInterface {
name = 'CreateUser1604164774154';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "file" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "path" character varying NOT NULL, CONSTRAINT "PK_36b46d232307066b3a2c9ea3a1d" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "role" ("id" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "status" ("id" integer NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_e12743a7086ec826733f54e1d95" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE TABLE "user" ("id" SERIAL NOT NULL, "email" character varying, "password" character varying, "provider" character varying NOT NULL DEFAULT 'email', "socialId" character varying, "firstName" character varying, "lastName" character varying, "hash" character varying, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "photoId" uuid, "roleId" integer, "statusId" integer, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"), CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_9bd2fe7a8e694dedc4ec2f666f" ON "user" ("socialId") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_58e4dbff0e1a32a9bdc861bb29" ON "user" ("firstName") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_f0e1b4ecdca13b177e2e3a0613" ON "user" ("lastName") `,
);
await queryRunner.query(
`CREATE INDEX "IDX_e282acb94d2e3aec10f480e4f6" ON "user" ("hash") `,
);
await queryRunner.query(
`CREATE TABLE "forgot" ("id" SERIAL NOT NULL, "hash" character varying NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP, "userId" integer, CONSTRAINT "PK_087959f5bb89da4ce3d763eab75" PRIMARY KEY ("id"))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_df507d27b0fb20cd5f7bef9b9a" ON "forgot" ("hash") `,
);
await queryRunner.query(
`ALTER TABLE "user" ADD CONSTRAINT "FK_75e2be4ce11d447ef43be0e374f" FOREIGN KEY ("photoId") REFERENCES "file"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "user" ADD CONSTRAINT "FK_c28e52f758e7bbc53828db92194" FOREIGN KEY ("roleId") REFERENCES "role"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "user" ADD CONSTRAINT "FK_dc18daa696860586ba4667a9d31" FOREIGN KEY ("statusId") REFERENCES "status"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
await queryRunner.query(
`ALTER TABLE "forgot" ADD CONSTRAINT "FK_31f3c80de0525250f31e23a9b83" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "forgot" DROP CONSTRAINT "FK_31f3c80de0525250f31e23a9b83"`,
);
await queryRunner.query(
`ALTER TABLE "user" DROP CONSTRAINT "FK_dc18daa696860586ba4667a9d31"`,
);
await queryRunner.query(
`ALTER TABLE "user" DROP CONSTRAINT "FK_c28e52f758e7bbc53828db92194"`,
);
await queryRunner.query(
`ALTER TABLE "user" DROP CONSTRAINT "FK_75e2be4ce11d447ef43be0e374f"`,
);
await queryRunner.query(`DROP INDEX "IDX_df507d27b0fb20cd5f7bef9b9a"`);
await queryRunner.query(`DROP TABLE "forgot"`);
await queryRunner.query(`DROP INDEX "IDX_e282acb94d2e3aec10f480e4f6"`);
await queryRunner.query(`DROP INDEX "IDX_f0e1b4ecdca13b177e2e3a0613"`);
await queryRunner.query(`DROP INDEX "IDX_58e4dbff0e1a32a9bdc861bb29"`);
await queryRunner.query(`DROP INDEX "IDX_9bd2fe7a8e694dedc4ec2f666f"`);
await queryRunner.query(`DROP TABLE "user"`);
await queryRunner.query(`DROP TABLE "status"`);
await queryRunner.query(`DROP TABLE "role"`);
await queryRunner.query(`DROP TABLE "file"`);
}
}

View File

@@ -0,0 +1,29 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddTeamPublicAccess1785513600000 implements MigrationInterface {
name = 'AddTeamPublicAccess1785513600000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "team" ADD "publicAccessEnabled" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "team" ADD "publicAccessToken" character varying(64)`,
);
await queryRunner.query(
`ALTER TABLE "team" ADD CONSTRAINT "UQ_team_public_access_token" UNIQUE ("publicAccessToken")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "team" DROP CONSTRAINT "UQ_team_public_access_token"`,
);
await queryRunner.query(
`ALTER TABLE "team" DROP COLUMN "publicAccessToken"`,
);
await queryRunner.query(
`ALTER TABLE "team" DROP COLUMN "publicAccessEnabled"`,
);
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { Team } from 'src/teams/entities/team.entity';
import { User } from 'src/users/entities/user.entity';
import { PlayersSeedService } from './player-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([Player, Team, User])],
providers: [PlayersSeedService],
exports: [PlayersSeedService],
})
export class PlayersSeedModule {}

View File

@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { Team } from 'src/teams/entities/team.entity';
import { Repository } from 'typeorm';
import { faker } from '@faker-js/faker';
import { User } from 'src/users/entities/user.entity';
@Injectable()
export class PlayersSeedService {
constructor(
@InjectRepository(Player)
private repository: Repository<Player>,
@InjectRepository(Team)
private teamRepository: Repository<Team>,
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
async run() {
const dev = await this.teamRepository.findOne({ where: { id: 1 } });
const countPlayer = await this.repository.count();
if (countPlayer === 0) {
const players = [];
for (let index = 0; index < 20; index++) {
const p = this.repository.create({
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
teamRole: {
id: 1,
name: 'player',
},
team: dev,
});
players.push(p);
}
const p2 = this.repository.create({
firstName: 'Player',
lastName: 'Kapitän',
teamRole: {
id: 3,
name: 'captain',
},
team: dev,
});
const u = await this.userRepository.findOne({
where: { email: 'mail@bastian-wagner.de' },
});
const p3 = this.repository.create({
firstName: 'Player',
lastName: 'Kassenwart',
teamRole: {
id: 4,
name: 'treasurer',
},
team: dev,
user: u,
});
await this.repository.save(players);
await this.repository.save([p2, p3]);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Role } from 'src/roles/entities/role.entity';
import { RoleSeedService } from './role-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([Role])],
providers: [RoleSeedService],
exports: [RoleSeedService],
})
export class RoleSeedModule {}

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Role } from 'src/roles/entities/role.entity';
import { RoleEnum } from 'src/roles/roles.enum';
import { Repository } from 'typeorm';
@Injectable()
export class RoleSeedService {
constructor(
@InjectRepository(Role)
private repository: Repository<Role>,
) {}
async run() {
const countPlayer = await this.repository.count({
where: {
id: RoleEnum.user,
},
});
if (countPlayer === 0) {
await this.repository.save(
this.repository.create({
id: RoleEnum.user,
name: 'user',
}),
);
}
const countAdmins = await this.repository.count({
where: {
id: RoleEnum.admin,
},
});
if (countAdmins === 0) {
await this.repository.save(
this.repository.create({
id: RoleEnum.admin,
name: 'admin',
}),
);
}
}
}

View File

@@ -0,0 +1,33 @@
import { NestFactory } from '@nestjs/core';
import { PlayersSeedService } from './player/player-seed.service';
import { RoleSeedService } from './role/role-seed.service';
import { SeedModule } from './seed.module';
import { StatusSeedService } from './status/status-seed.service';
import { TeamRoleSeedService } from './team-role/team-role-seed.service';
import { TeamsSettingsSeedService } from './team-settings/team-settings-seed.service';
import { TeamWalletTransactionTypeSeedService } from './team-wallet-transactions-type/team-wallet-transaction-type-seed.service';
import { TeamWalletTransactionSeedService } from './team-wallet-transactions/team-wallet-transaction-seed.service';
import { TeamsSeedService } from './teams/teams-seed.service';
import { TransactionsTypeSeedService } from './transaction-type/transactions-type-seed.service';
import { TransactionsSeedService } from './transactions/transactions-seed.service';
import { UserSeedService } from './user/user-seed.service';
const runSeed = async () => {
const app = await NestFactory.create(SeedModule);
// run
await app.get(RoleSeedService).run();
await app.get(StatusSeedService).run();
await app.get(UserSeedService).run();
await app.get(TeamRoleSeedService).run();
await app.get(TeamsSeedService).run();
await app.get(PlayersSeedService).run();
await app.get(TransactionsTypeSeedService).run();
await app.get(TransactionsSeedService).run();
await app.get(TeamsSettingsSeedService).run();
await app.get(TeamWalletTransactionTypeSeedService).run();
await app.get(TeamWalletTransactionSeedService).run();
await app.close();
};
void runSeed();

View File

@@ -0,0 +1,47 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import appConfig from 'src/config/app.config';
import databaseConfig from 'src/config/database.config';
import { DataSource } from 'typeorm';
import { TypeOrmConfigService } from '../typeorm-config.service';
import { PlayersSeedModule } from './player/player-seed.module';
import { RoleSeedModule } from './role/role-seed.module';
import { StatusSeedModule } from './status/status-seed.module';
import { TeamRolesSeedModule } from './team-role/team-role-seed.module';
import { TeamSettingsSeedModule } from './team-settings/team-settings-seed.module';
import { TeamWalletTransactionTypeSeedModule } from './team-wallet-transactions-type/team-wallet-transaction-type-seed.module';
import { TeamWalletTransactionSeedModule } from './team-wallet-transactions/team-wallet-transaction-seed.module';
import { TeamsSeedModule } from './teams/teams-seed.module';
import { TransactionsTypeSeedModule } from './transaction-type/transactions-type-seed.module';
import { TransactionsSeedModule } from './transactions/transactions-seed.module';
import { UserSeedModule } from './user/user-seed.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [databaseConfig, appConfig],
envFilePath: ['.env'],
}),
TypeOrmModule.forRootAsync({
useClass: TypeOrmConfigService,
dataSourceFactory: async (options) => {
const dataSource = await new DataSource(options).initialize();
return dataSource;
},
}),
RoleSeedModule,
StatusSeedModule,
UserSeedModule,
TeamRolesSeedModule,
TeamsSeedModule,
PlayersSeedModule,
TransactionsTypeSeedModule,
TransactionsSeedModule,
TeamSettingsSeedModule,
TeamWalletTransactionTypeSeedModule,
TeamWalletTransactionSeedModule,
],
})
export class SeedModule {}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Status } from 'src/statuses/entities/status.entity';
import { StatusSeedService } from './status-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([Status])],
providers: [StatusSeedService],
exports: [StatusSeedService],
})
export class StatusSeedModule {}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Status } from 'src/statuses/entities/status.entity';
import { StatusEnum } from 'src/statuses/statuses.enum';
import { Repository } from 'typeorm';
@Injectable()
export class StatusSeedService {
constructor(
@InjectRepository(Status)
private repository: Repository<Status>,
) {}
async run() {
const count = await this.repository.count();
if (count === 0) {
await this.repository.save([
this.repository.create({
id: StatusEnum.active,
name: 'Active',
}),
this.repository.create({
id: StatusEnum.inactive,
name: 'Inactive',
}),
]);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
import { TeamRoleSeedService } from './team-role-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([TeamRole])],
providers: [TeamRoleSeedService],
exports: [TeamRoleSeedService],
})
export class TeamRolesSeedModule {}

View File

@@ -0,0 +1,90 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
import { TeamRolesEnum } from 'src/team-roles/team-roles.enum';
import { Repository } from 'typeorm';
@Injectable()
export class TeamRoleSeedService {
constructor(
@InjectRepository(TeamRole)
private repository: Repository<TeamRole>,
) {}
async run() {
const countPlayer = await this.repository.count({
where: {
id: TeamRolesEnum.player,
},
});
if (countPlayer === 0) {
await this.repository.save(
this.repository.create({
id: TeamRolesEnum.player,
name: 'player',
}),
);
}
const countScndTr = await this.repository.count({
where: {
id: TeamRolesEnum.scnd_treasurer,
},
});
if (countScndTr === 0) {
await this.repository.save(
this.repository.create({
id: TeamRolesEnum.scnd_treasurer,
name: 'scnd_treasurer',
}),
);
}
const countCaptain = await this.repository.count({
where: {
id: TeamRolesEnum.captain,
},
});
if (countCaptain === 0) {
await this.repository.save(
this.repository.create({
id: TeamRolesEnum.captain,
name: 'captain',
}),
);
}
const countTreasurer = await this.repository.count({
where: {
id: TeamRolesEnum.treasurer,
},
});
if (countTreasurer === 0) {
await this.repository.save(
this.repository.create({
id: TeamRolesEnum.treasurer,
name: 'treasurer',
}),
);
}
const countCoach = await this.repository.count({
where: {
id: TeamRolesEnum.coach,
},
});
if (countCoach === 0) {
await this.repository.save(
this.repository.create({
id: TeamRolesEnum.coach,
name: 'coach',
}),
);
}
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
import { Team } from 'src/teams/entities/team.entity';
import { TeamsSettingsSeedService } from './team-settings-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([Team, TeamSetting])],
providers: [TeamsSettingsSeedService],
exports: [TeamsSettingsSeedService],
})
export class TeamSettingsSeedModule {}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TeamSetting } from 'src/team-settings/entities/team-setting.entity';
import { Team } from 'src/teams/entities/team.entity';
import { Repository } from 'typeorm';
@Injectable()
export class TeamsSettingsSeedService {
constructor(
@InjectRepository(TeamSetting)
private repository: Repository<TeamSetting>,
@InjectRepository(Team)
private teamrepository: Repository<Team>,
) {}
async run() {
const countTeamSettings = await this.repository.count();
if (countTeamSettings === 0) {
const team = await this.teamrepository.findOne({ where: { id: 1 } });
await this.repository.save(
this.repository.create({
team,
key: 'transaction_create_min_role',
value: '2',
}),
);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TeamWalletTransactionType } from 'src/team-wallet-transactions/entities/team-wallet-transaction-type.entity';
import { TeamWalletTransactionTypeSeedService } from './team-wallet-transaction-type-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([TeamWalletTransactionType])],
providers: [TeamWalletTransactionTypeSeedService],
exports: [TeamWalletTransactionTypeSeedService],
})
export class TeamWalletTransactionTypeSeedModule {}

View File

@@ -0,0 +1,37 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TeamWalletTransactionType } from 'src/team-wallet-transactions/entities/team-wallet-transaction-type.entity';
import { TeamWalletTransactionEnum } from 'src/team-wallet-transactions/team-wallet-transaction.enum';
import { Repository } from 'typeorm';
@Injectable()
export class TeamWalletTransactionTypeSeedService {
constructor(
@InjectRepository(TeamWalletTransactionType)
private repository: Repository<TeamWalletTransactionType>,
) {}
async run() {
const amount = await this.repository.count({
where: {
id: 0,
},
});
if (amount === 0) {
await this.repository.save(
this.repository.create({
id: TeamWalletTransactionEnum.credit,
name: 'credit',
}),
);
await this.repository.save(
this.repository.create({
id: TeamWalletTransactionEnum.expense,
name: 'expense',
}),
);
}
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { Team } from 'src/teams/entities/team.entity';
import { TeamWalletTransactionSeedService } from './team-wallet-transaction-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([TeamWalletTransaction, Team])],
providers: [TeamWalletTransactionSeedService],
exports: [TeamWalletTransactionSeedService],
})
export class TeamWalletTransactionSeedModule {}

View File

@@ -0,0 +1,44 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { TeamWalletTransactionEnum } from 'src/team-wallet-transactions/team-wallet-transaction.enum';
import { Team } from 'src/teams/entities/team.entity';
import { Repository } from 'typeorm';
import { faker } from '@faker-js/faker';
@Injectable()
export class TeamWalletTransactionSeedService {
constructor(
@InjectRepository(TeamWalletTransaction)
private repository: Repository<TeamWalletTransaction>,
@InjectRepository(Team)
private teamrepository: Repository<Team>,
) {}
async run() {
const amount = await this.repository.count({
where: {
id: 0,
},
});
if (amount === 0) {
const team = await this.teamrepository.findOneBy({ id: 1 });
for (let index = 0; index < 20; index++) {
await this.repository.save(
this.repository.create({
team: team,
note: '',
date: faker.date.recent(90).toISOString(),
amount: Math.random() * 10,
type: {
id: TeamWalletTransactionEnum.credit,
name: 'credit',
},
}),
);
}
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Team } from 'src/teams/entities/team.entity';
import { TeamsSeedService } from './teams-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([Team])],
providers: [TeamsSeedService],
exports: [TeamsSeedService],
})
export class TeamsSeedModule {}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Team } from 'src/teams/entities/team.entity';
import { Repository } from 'typeorm';
@Injectable()
export class TeamsSeedService {
constructor(
@InjectRepository(Team)
private repository: Repository<Team>,
) {}
async run() {
const countTeams = await this.repository.count({
where: {
id: 1,
},
});
if (countTeams === 0) {
await this.repository.save(
this.repository.create({
id: 1,
name: 'Development Team',
alias: '9999',
}),
);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TransactionType } from 'src/transactions/entitites/transaction-type.entity';
import { TransactionsTypeSeedService } from './transactions-type-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([TransactionType])],
providers: [TransactionsTypeSeedService],
exports: [TransactionsTypeSeedService],
})
export class TransactionsTypeSeedModule {}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { TransactionType } from 'src/transactions/entitites/transaction-type.entity';
import { TransactionTypeEnum } from 'src/transactions/transaction-type.enum';
import { Repository } from 'typeorm';
@Injectable()
export class TransactionsTypeSeedService {
constructor(
@InjectRepository(TransactionType)
private repository: Repository<TransactionType>,
) {}
async run() {
const count = await this.repository.count();
if (count === 0) {
await this.repository.save([
this.repository.create({
id: TransactionTypeEnum.credit,
name: 'credit',
}),
this.repository.create({
id: TransactionTypeEnum.fee,
name: 'fee',
}),
this.repository.create({
id: TransactionTypeEnum.fine,
name: 'fine',
}),
this.repository.create({
id: TransactionTypeEnum.levy,
name: 'levy',
}),
this.repository.create({
id: TransactionTypeEnum.payment,
name: 'payment',
}),
]);
}
}
}

View File

@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { TransactionType } from 'src/transactions/entitites/transaction-type.entity';
import { Transaction } from 'src/transactions/entitites/transaction.entity';
import { TransactionsSeedService } from './transactions-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([TransactionType, Transaction, Player])],
providers: [TransactionsSeedService],
exports: [TransactionsSeedService],
})
export class TransactionsSeedModule {}

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Player } from 'src/players/entities/player.entity';
import { TransactionType } from 'src/transactions/entitites/transaction-type.entity';
import { Transaction } from 'src/transactions/entitites/transaction.entity';
import { Repository } from 'typeorm';
import { faker } from '@faker-js/faker';
@Injectable()
export class TransactionsSeedService {
constructor(
@InjectRepository(Transaction)
private repository: Repository<Transaction>,
@InjectRepository(Player)
private playerRepository: Repository<Player>,
@InjectRepository(TransactionType)
private transactionTypeRepository: Repository<TransactionType>,
) {}
async run() {
const count = await this.repository.count();
if (count === 0) {
const players = await this.playerRepository.find();
const transactionTypes = await this.transactionTypeRepository.find();
const transactions = [];
for (let x = 0; x < 500; x++) {
const p = Math.floor(Math.random() * players.length);
const type = Math.floor(Math.random() * transactionTypes.length);
const t = this.repository.create({
amount: Math.random() * 50,
player: players[p],
type: transactionTypes[type],
note: faker.hacker.phrase(),
date: faker.date.recent(90).toISOString(),
});
transactions.push(t);
}
await this.repository.save(transactions);
}
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { UserSeedService } from './user-seed.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserSeedService],
exports: [UserSeedService],
})
export class UserSeedModule {}

View File

@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { RoleEnum } from 'src/roles/roles.enum';
import { StatusEnum } from 'src/statuses/statuses.enum';
import { User } from 'src/users/entities/user.entity';
import { Repository } from 'typeorm';
@Injectable()
export class UserSeedService {
constructor(
@InjectRepository(User)
private repository: Repository<User>,
) {}
async run() {
const countAdmin = await this.repository.count({
where: {
role: {
id: RoleEnum.admin,
},
},
});
if (countAdmin === 0) {
await this.repository.save(
this.repository.create({
firstName: 'Bastian',
lastName: 'Wagner',
email: 'mail@bastian-wagner.de',
password: 'Passwort123',
role: {
id: RoleEnum.admin,
name: 'Admin',
},
status: {
id: StatusEnum.active,
name: 'Active',
},
}),
);
}
const countUser = await this.repository.count({
where: {
role: {
id: RoleEnum.user,
},
},
});
if (countUser === 0) {
await this.repository.save(
this.repository.create({
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
password: 'secret',
role: {
id: RoleEnum.user,
name: 'Admin',
},
status: {
id: StatusEnum.active,
name: 'Active',
},
}),
);
}
}
}

View File

@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
constructor(private configService: ConfigService) {}
createTypeOrmOptions(): TypeOrmModuleOptions {
return {
type: this.configService.get('database.type'),
url: this.configService.get('database.url'),
host: this.configService.get('database.host'),
port: this.configService.get('database.port'),
username: this.configService.get('database.username'),
password: this.configService.get('database.password'),
database: this.configService.get('database.name'),
synchronize: this.configService.get('database.synchronize'),
dropSchema: false,
keepConnectionAlive: true,
logging: this.configService.get('app.nodeEnv') !== 'prod',
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/**/*{.ts,.js}'],
cli: {
entitiesDir: 'src',
migrationsDir: 'src/database/migrations',
subscribersDir: 'subscriber',
},
} as TypeOrmModuleOptions;
}
}

View File

@@ -0,0 +1,30 @@
import {
Column,
Entity,
PrimaryGeneratedColumn,
AfterLoad,
AfterInsert,
} from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { Allow } from 'class-validator';
import { EntityHelper } from 'src/utils/entity-helper';
import appConfig from '../../config/app.config';
@Entity({ name: 'file' })
export class FileEntity extends EntityHelper {
@ApiProperty({ example: 'cbcfa8b8-3a25-4adb-a9c6-e325f0d0f3ae' })
@PrimaryGeneratedColumn('uuid')
id: string;
@Allow()
@Column()
path: string;
@AfterLoad()
@AfterInsert()
updatePath() {
if (this.path.indexOf('/') === 0) {
this.path = appConfig().backendDomain + this.path;
}
}
}

View File

@@ -0,0 +1,48 @@
import {
Controller,
Get,
Param,
Post,
Response,
UploadedFile,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { AuthGuard } from '@nestjs/passport';
import { FilesService } from './files.service';
@ApiTags('Files')
@Controller({
path: 'files',
version: '1',
})
export class FilesController {
constructor(private readonly filesService: FilesService) {}
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
@Post('upload')
@ApiConsumes('multipart/form-data')
@ApiBody({
schema: {
type: 'object',
properties: {
file: {
type: 'string',
format: 'binary',
},
},
},
})
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file) {
return this.filesService.uploadFile(file);
}
@Get(':path')
download(@Param('path') path, @Response() response) {
return response.sendFile(path, { root: './files' });
}
}

View File

@@ -0,0 +1,90 @@
import { HttpException, HttpStatus, Module } from '@nestjs/common';
import { FilesController } from './files.controller';
import { MulterModule } from '@nestjs/platform-express';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { diskStorage } from 'multer';
import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
import * as AWS from 'aws-sdk';
import * as multerS3 from 'multer-s3';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FileEntity } from './entities/file.entity';
import { FilesService } from './files.service';
@Module({
imports: [
TypeOrmModule.forFeature([FileEntity]),
MulterModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => {
const storages = {
local: () =>
diskStorage({
destination: './files',
filename: (request, file, callback) => {
callback(
null,
`${randomStringGenerator()}.${file.originalname
.split('.')
.pop()
.toLowerCase()}`,
);
},
}),
s3: () => {
const s3 = new AWS.S3();
AWS.config.update({
accessKeyId: configService.get('file.accessKeyId'),
secretAccessKey: configService.get('file.secretAccessKey'),
region: configService.get('file.awsS3Region'),
});
return multerS3({
s3: s3,
bucket: configService.get('file.awsDefaultS3Bucket'),
acl: 'public-read',
contentType: multerS3.AUTO_CONTENT_TYPE,
key: (request, file, callback) => {
callback(
null,
`${randomStringGenerator()}.${file.originalname
.split('.')
.pop()
.toLowerCase()}`,
);
},
});
},
};
return {
fileFilter: (request, file, callback) => {
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/i)) {
return callback(
new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
file: `cantUploadFileType`,
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
),
false,
);
}
callback(null, true);
},
storage: storages[configService.get('file.driver')](),
limits: {
fileSize: configService.get('file.maxFileSize'),
},
};
},
}),
],
controllers: [FilesController],
providers: [ConfigModule, ConfigService, FilesService],
})
export class FilesModule {}

View File

@@ -0,0 +1,39 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { FileEntity } from './entities/file.entity';
import { Repository } from 'typeorm';
@Injectable()
export class FilesService {
constructor(
private readonly configService: ConfigService,
@InjectRepository(FileEntity)
private fileRepository: Repository<FileEntity>,
) {}
async uploadFile(file): Promise<FileEntity> {
if (!file) {
throw new HttpException(
{
status: HttpStatus.UNPROCESSABLE_ENTITY,
errors: {
file: 'selectFile',
},
},
HttpStatus.UNPROCESSABLE_ENTITY,
);
}
const path = {
local: `/${this.configService.get('app.apiPrefix')}/v1/${file.path}`,
s3: file.location,
};
return this.fileRepository.save(
this.fileRepository.create({
path: path[this.configService.get('file.driver')],
}),
);
}
}

View File

@@ -0,0 +1,35 @@
import {
Column,
CreateDateColumn,
Entity,
Index,
ManyToOne,
PrimaryGeneratedColumn,
DeleteDateColumn,
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Allow } from 'class-validator';
import { EntityHelper } from 'src/utils/entity-helper';
@Entity()
export class Forgot extends EntityHelper {
@PrimaryGeneratedColumn()
id: number;
@Allow()
@Column()
@Index()
hash: string;
@Allow()
@ManyToOne(() => User, {
eager: true,
})
user: User;
@CreateDateColumn()
createdAt: Date;
@DeleteDateColumn()
deletedAt: Date;
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Forgot } from './entities/forgot.entity';
import { ForgotService } from './forgot.service';
@Module({
imports: [TypeOrmModule.forFeature([Forgot])],
providers: [ForgotService],
exports: [ForgotService],
})
export class ForgotModule {}

View File

@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DeepPartial } from 'src/utils/types/deep-partial.type';
import { FindOptions } from 'src/utils/types/find-options.type';
import { Repository } from 'typeorm';
import { Forgot } from './entities/forgot.entity';
@Injectable()
export class ForgotService {
constructor(
@InjectRepository(Forgot)
private forgotRepository: Repository<Forgot>,
) {}
async findOne(options: FindOptions<Forgot>) {
return this.forgotRepository.findOne({
where: options.where,
});
}
async findMany(options: FindOptions<Forgot>) {
return this.forgotRepository.find({
where: options.where,
});
}
async create(data: DeepPartial<Forgot>) {
return this.forgotRepository.save(this.forgotRepository.create(data));
}
async softDelete(id: number): Promise<void> {
await this.forgotRepository.softDelete(id);
}
}

View File

@@ -0,0 +1,15 @@
import { Controller, Get } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { HomeService } from './home.service';
@ApiTags('Home')
@Controller()
export class HomeController {
constructor(private service: HomeService) {}
@Get()
appInfo() {
return this.service.appInfo();
}
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { HomeService } from './home.service';
import { HomeController } from './home.controller';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule],
controllers: [HomeController],
providers: [HomeService],
})
export class HomeModule {}

View File

@@ -0,0 +1,11 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class HomeService {
constructor(private configService: ConfigService) {}
appInfo() {
return { name: this.configService.get('app.name') };
}
}

View File

@@ -0,0 +1,4 @@
{
"confirmEmail": "Confirm email",
"resetPassword": "Reset password"
}

View File

@@ -0,0 +1,5 @@
{
"text1": "Hey!",
"text2": "Youre almost ready to start enjoying",
"text3": "Simply click the big green button below to verify your email address."
}

View File

@@ -0,0 +1,6 @@
{
"text1": "Trouble signing in?",
"text2": "Resetting your password is easy.",
"text3": "Just press the button below and follow the instructions. Well have you up and running in no time.",
"text4": "If you did not make this request then please ignore this email."
}

View File

@@ -0,0 +1,4 @@
export interface MailData<T = never> {
to: string;
data: T;
}

View File

@@ -0,0 +1,43 @@
import * as path from 'path';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { MailerOptions, MailerOptionsFactory } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
@Injectable()
export class MailConfigService implements MailerOptionsFactory {
constructor(private configService: ConfigService) {}
createMailerOptions(): MailerOptions {
return {
transport: {
host: this.configService.get('mail.host'),
port: this.configService.get('mail.port'),
ignoreTLS: this.configService.get('mail.ignoreTLS'),
secure: this.configService.get('mail.secure'),
requireTLS: this.configService.get('mail.requireTLS'),
auth: {
user: this.configService.get('mail.user'),
pass: this.configService.get('mail.password'),
},
},
defaults: {
from: `"${this.configService.get(
'mail.defaultName',
)}" <${this.configService.get('mail.defaultEmail')}>`,
},
template: {
dir: path.join(
this.configService.get('app.workingDirectory'),
'src',
'mail',
'mail-templates',
),
adapter: new HandlebarsAdapter(),
options: {
strict: true,
},
},
} as MailerOptions;
}
}

View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=">
<title>{{title}}</title>
</head>
<body style="margin:0;font-family:arial">
<table style="border:0;width:100%">
<tr style="background:#eeeeee">
<td style="padding:20px;color:#808080;text-align:center;font-size:40px;font-weight:600">
{{app_name}}
</td>
</tr>
<tr>
<td style="padding:20px;color:#808080;font-size:16px;font-weight:100">
{{text1}}<br>
{{text2}} {{app_name}}.<br>
{{text3}}
</td>
</tr>
<tr>
<td style="text-align:center">
<a href="{{url}}"
style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{actionTitle}}</a>
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=">
<title>{{title}}</title>
</head>
<body style="margin:0;font-family:arial">
<table style="border:0;width:100%">
<tr style="background:#eeeeee">
<td style="padding:20px;color:#808080;text-align:center;font-size:40px;font-weight:600">
{{app_name}}
</td>
</tr>
<tr>
<td style="padding:20px;color:#808080;font-size:16px;font-weight:100">
{{text1}}<br>
{{text2}}<br>
{{text3}}
</td>
</tr>
<tr>
<td style="text-align:center">
<a href="{{url}}"
style="display:inline-block;padding:20px;background:#00838f;text-decoration:none;color:#ffffff">{{actionTitle}}</a>
</td>
</tr>
<tr>
<td style="padding:20px;color:#808080;font-size:16px;font-weight:100">
{{text4}}
</td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MailService } from './mail.service';
@Module({
imports: [ConfigModule],
providers: [MailService],
exports: [MailService],
})
export class MailModule {}

View File

@@ -0,0 +1,62 @@
import { MailerService } from '@nestjs-modules/mailer';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { I18n, I18nRequestScopeService } from 'nestjs-i18n';
import { MailData } from './interfaces/mail-data.interface';
@Injectable()
export class MailService {
constructor(
@I18n()
private i18n: I18nRequestScopeService,
private mailerService: MailerService,
private configService: ConfigService,
) {}
async userSignUp(mailData: MailData<{ hash: string }>) {
return;
await this.mailerService.sendMail({
to: mailData.to,
subject: await this.i18n.t('common.confirmEmail'),
text: `${this.configService.get('app.frontendDomain')}/confirm-email/${
mailData.data.hash
} ${await this.i18n.t('common.confirmEmail')}`,
template: 'activation',
context: {
title: await this.i18n.t('common.confirmEmail'),
url: `${this.configService.get('app.frontendDomain')}/confirm-email/${
mailData.data.hash
}`,
actionTitle: await this.i18n.t('common.confirmEmail'),
app_name: this.configService.get('app.name'),
text1: await this.i18n.t('confirm-email.text1'),
text2: await this.i18n.t('confirm-email.text2'),
text3: await this.i18n.t('confirm-email.text3'),
},
});
}
async forgotPassword(mailData: MailData<{ hash: string }>) {
return;
await this.mailerService.sendMail({
to: mailData.to,
subject: await this.i18n.t('common.resetPassword'),
text: `${this.configService.get('app.frontendDomain')}/password-change/${
mailData.data.hash
} ${await this.i18n.t('common.resetPassword')}`,
template: 'reset-password',
context: {
title: await this.i18n.t('common.resetPassword'),
url: `${this.configService.get('app.frontendDomain')}/password-change/${
mailData.data.hash
}`,
actionTitle: await this.i18n.t('common.resetPassword'),
app_name: this.configService.get('app.name'),
text1: await this.i18n.t('reset-password.text1'),
text2: await this.i18n.t('reset-password.text2'),
text3: await this.i18n.t('reset-password.text3'),
text4: await this.i18n.t('reset-password.text4'),
},
});
}
}

View File

@@ -0,0 +1,35 @@
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { useContainer } from 'class-validator';
import { AppModule } from './app.module';
import validationOptions from './utils/validation-options';
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
useContainer(app.select(AppModule), { fallbackOnErrors: true });
const configService = app.get(ConfigService);
app.enableShutdownHooks();
app.setGlobalPrefix(configService.get('app.apiPrefix'), {
exclude: ['/'],
});
app.enableVersioning({
type: VersioningType.URI,
});
app.useGlobalPipes(new ValidationPipe(validationOptions));
const options = new DocumentBuilder()
.setTitle('API')
.setDescription('API docs')
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('docs', app, document);
await app.listen(configService.get('app.port'));
}
void bootstrap();

View File

@@ -0,0 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
import { Team } from 'src/teams/entities/team.entity';
export class CreatePenaltyDTO {
@ApiProperty({ example: 2342 })
@IsNotEmpty()
teamId: number;
@ApiProperty({ example: 1 })
@IsNotEmpty()
amount: number;
team?: Team;
@ApiProperty({ example: 'Zu spät kommen' })
@IsNotEmpty()
description: string;
}

View File

@@ -0,0 +1,29 @@
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { EntityHelper } from 'src/utils/entity-helper';
import { Team } from 'src/teams/entities/team.entity';
@Entity()
export class PenaltyEntity extends EntityHelper {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Team, {
eager: false,
})
team: Team;
@Column({ default: '' })
description: string;
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
amount: number;
@CreateDateColumn()
createdAt: Date;
}

View File

@@ -0,0 +1,45 @@
import {
Body,
Controller,
Get,
Param,
Post,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth } from '@nestjs/swagger';
import { Roles } from 'src/roles/roles.decorator';
import { RolesGuard } from 'src/roles/roles.guard';
import { CreatePenaltyDTO } from './dto/create-penalty.dto';
import { PenaltyService } from './penalty.service';
@ApiBearerAuth()
@Controller({
path: 'penalty',
version: '1',
})
export class PenaltyController {
constructor(private service: PenaltyService) {}
@Roles([])
@Get()
getIt(@Req() req: any) {
const userId = req.user?.id;
return this.service.getAll(userId);
}
@Roles([])
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Get(':id')
getTeams(@Param('id') teamId: string) {
return this.service.getTeamPenalties(teamId);
}
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Post()
createPenalty(@Req() req: any, @Body() createPenaltyDto: CreatePenaltyDTO) {
const userId = req.user?.id;
return this.service.createPenalty(createPenaltyDto, userId);
}
}

View File

@@ -0,0 +1,30 @@
import { Module } from '@nestjs/common';
import { PenaltyController } from './penalty.controller';
import { PenaltyService } from './penalty.service';
import { LoggingModule } from 'src/database/logging/logging.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TeamWalletTransactionType } from 'src/team-wallet-transactions/entities/team-wallet-transaction-type.entity';
import { TeamWalletTransaction } from 'src/team-wallet-transactions/entities/team-wallet-transaction.entity';
import { Team } from 'src/teams/entities/team.entity';
import { User } from 'src/users/entities/user.entity';
import { Role } from 'src/roles/entities/role.entity';
import { PenaltyEntity } from './entities/penalty.entity';
import { Player } from 'src/players/entities/player.entity';
@Module({
controllers: [PenaltyController],
providers: [PenaltyService],
imports: [
TypeOrmModule.forFeature([
User,
Role,
TeamWalletTransaction,
TeamWalletTransactionType,
Team,
PenaltyEntity,
Player,
]),
LoggingModule,
],
})
export class PenaltyModule {}

View File

@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import { CreatePenaltyDTO } from './dto/create-penalty.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Team } from 'src/teams/entities/team.entity';
import { Repository } from 'typeorm';
import { PenaltyEntity } from './entities/penalty.entity';
import { Player } from 'src/players/entities/player.entity';
@Injectable()
export class PenaltyService {
constructor(
@InjectRepository(Team)
private teamRepository: Repository<Team>,
@InjectRepository(Player)
private playerRepository: Repository<Player>,
@InjectRepository(PenaltyEntity)
private repository: Repository<PenaltyEntity>,
) {}
async createPenalty(dto: CreatePenaltyDTO, userId: string) {
const player = await this.playerRepository.findOne({
where: { user: { id: Number(userId) }, team: { id: dto.teamId } },
relations: ['team'],
});
if (!player || !player.team) {
return;
}
dto.team = player.team;
const e = this.repository.create(dto);
return this.repository.save(e);
}
getAll(userId: string) {
const id = Number(userId);
return this.repository.find({
where: {
team: { players: { user: { id } } },
},
relations: ['team'],
});
}
async getTeamPenalties(teamId: string | number) {
teamId = Number(teamId);
const res = await this.repository.find({
where: {
team: { id: teamId },
},
});
return res.map((r) => {
r.amount = Number(r.amount);
return r;
});
}
}

View File

@@ -0,0 +1,66 @@
import {
AfterLoad,
BeforeInsert,
Column,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { EntityHelper } from 'src/utils/entity-helper';
import { TeamRole } from 'src/team-roles/entities/team-roles.entity';
import { Team } from 'src/teams/entities/team.entity';
import { User } from 'src/users/entities/user.entity';
import { Transaction } from 'src/transactions/entitites/transaction.entity';
@Entity()
export class Player extends EntityHelper {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@ManyToOne(() => TeamRole, {
eager: true,
})
teamRole?: TeamRole | null;
@ManyToOne(() => Team, {
eager: true,
})
team: Team;
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
balance: number;
@ManyToOne(() => User, (user) => user.players, {
eager: true,
})
user?: User | null;
@OneToMany(() => Transaction, (transaction) => transaction.player)
transactions: Transaction[];
@AfterLoad()
updateValue() {
this.balance = Number(this.balance);
}
@Column({ default: true })
active: boolean;
@BeforeInsert()
prepareData() {
if (this.balance == null) {
this.balance = 0;
}
if (this.transactions == null) {
this.transactions = [];
}
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'src/users/entities/user.entity';
import { Player } from './entities/player.entity';
@Module({
imports: [TypeOrmModule.forFeature([Player, User])],
})
export class PlayersModule {}

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