Merge pull request #3 from IT-Lab-KubSU/feature/auth

Feature/auth
main
Artem-Darius Weber 1 year ago committed by GitHub
commit 42b6fdc36b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -20,6 +20,8 @@
## Сервисы
- [frontend (ReactJS)](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/frontend)
- [AGW](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/agw)
- [User Data Service](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/user-data)
- [Auth Service](https://github.com/IT-Lab-KubSU/platform/tree/main/apps/auth)
<!-- todo: интегрировать SC Omni Services Embeding c списком всех компонентов -->
## Развертывание
@ -28,7 +30,7 @@ To start the development server run `nx serve frontend`. Open your browser and n
Run all services:
```
nx run-many --parallel --target=serve --projects=agw,frontend
nx run-many --parallel --target=serve --projects=agw,user-data,auth,frontend
```
<a style="padding: 10px; background: black; color: white; border-radius: 25px;" href="https://l.djft.ru/r/72/p/itlabplatform1/a/p/a/">
Deploy on SCOS
@ -47,6 +49,13 @@ graph TD;
agw-e2w-->agw;
frontend-e2e-->frontend;
frontend-->agw;
agw-->user-data;
user-data-->user-data-db;
user-data-db-->user-data-pg-admin;
agw-->auth-client;
auth-client-->auth;
auth-->auth-db;
auth-db-->auth-db-admin;
```
## Generate code

@ -0,0 +1,10 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
}
]
}

@ -0,0 +1,19 @@
/* eslint-disable */
export default {
displayName: 'auth-e2e',
preset: '../../jest.preset.js',
globalSetup: '<rootDir>/src/support/global-setup.ts',
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
setupFiles: ['<rootDir>/src/support/test-setup.ts'],
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/auth-e2e',
};

@ -0,0 +1,22 @@
{
"name": "auth-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"implicitDependencies": ["auth"],
"targets": {
"e2e": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"],
"options": {
"jestConfig": "apps/auth-e2e/jest.config.ts",
"passWithNoTests": true
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/auth-e2e/**/*.{js,ts}"]
}
}
}
}

@ -0,0 +1,10 @@
import axios from 'axios';
describe('GET /api', () => {
it('should return a message', async () => {
const res = await axios.get(`/api`);
expect(res.status).toBe(200);
expect(res.data).toEqual({ message: 'Hello API' });
});
});

@ -0,0 +1,10 @@
/* eslint-disable */
var __TEARDOWN_MESSAGE__: string;
module.exports = async function () {
// Start services that that the app needs to run (e.g. database, docker-compose, etc.).
console.log('\nSetting up...\n');
// Hint: Use `globalThis` to pass variables to global teardown.
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
};

@ -0,0 +1,7 @@
/* eslint-disable */
module.exports = async function () {
// Put clean up logic here (e.g. stopping services, docker-compose, etc.).
// Hint: `globalThis` is shared between setup and teardown.
console.log(globalThis.__TEARDOWN_MESSAGE__);
};

@ -0,0 +1,10 @@
/* eslint-disable */
import axios from 'axios';
module.exports = async function () {
// Configure axios for tests to use.
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? '3000';
axios.defaults.baseURL = `http://${host}:${port}`;
};

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.ts"]
}

@ -0,0 +1,13 @@
$$
@version: 0.1.0;
@scuuid: fbc45229-30c1-4daf-8f28-063bf27ef7d3;
@type: service;
@platform: nestjs;
@license: BSD-3-Clause;
@owner: artem-darius weber;
@author: ;
@title: user-authentication;
@desc: ;
@rp: kubsu it lab;
@vr: 7093;
$$

@ -0,0 +1,6 @@
PORT=3005
DATABASE_URL=postgresql://postgres:postgres@localhost:5433/user-authentication
PUBLIC_KEY_PATH=tokens/jwt.key.pub
PRIVATE_KEY_PATH=tokens/jwt.key

@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

335
apps/auth/.gitignore vendored

@ -0,0 +1,335 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# user-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Xcode template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Gcc Patch
/*.gcno
### Example user template template
### Example user template
# IntelliJ project files
.idea
*.iml
out
gen
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
#out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### OpenSSL template
# OpenSSL-related files best not committed
## Certificate Authority
*.ca
## Certificate
*.crt
## Certificate Sign Request
*.csr
## Certificate
*.der
## Key database file
*.kdb
## OSCP request data
*.org
## PKCS #12
*.p12
## PEM-encoded certificate data
*.pem
## Random number seed
*.rnd
## SSLeay data
*.ssleay
## S/MIME message
*.smime
*.env
*.env.backup
*.save
# compiled output
/dist
/node_modules
# 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
tokens

@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1
FROM node:18
WORKDIR /app
COPY --chown=node:node . .
RUN rm -f *.env *.env.*
RUN apt-get update -y && apt-get install -y dumb-init
RUN npm install
ENV NODE_ENV=production
RUN npm run prefullbuild && npm run prebuild && npm run build
RUN mkdir temp temp/.prisma temp/@prisma temp/prisma && cp -r ./node_modules/.prisma/* ./temp/.prisma/ && cp -r ./node_modules/@prisma/* ./temp/@prisma/ && cp -r ./node_modules/prisma/* ./temp/prisma/
RUN rm -rdf node_modules
RUN npm install --production
RUN cp -r ./temp/* ./node_modules/ && rm -rdf temp
RUN ls | grep -v node_modules | grep -v dist | xargs rm -rfv
RUN cp -r ./dist/* ./ && rm -rdf dist
USER node
CMD ["dumb-init", "node", "./main.js"]

@ -0,0 +1,11 @@
Copyright 2023 SC (DJEEFT) ©
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,40 @@
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center;">
<img style="border-radius: 1em; align-self: center;" src="https://drive.google.com/uc?export=view&id=1_xQWdF3RtL1MRcdDESMPXMBiAQDHCDGT" width="45">
<h1 style="text-align: center; margin-left: 10px; margin-top: 2px;">IT Lab Platform - Auth Service</h1>
</div>
<div style="display: flex; align-items: center; border: 1px solid #ccc; padding: 10px; border-radius: 5px;">
<span style="font-size: 20px; margin-right: 10px;"></span>
<span style="font-size: 16px;">
Эта директория содержит исходный код системы аунтификации приложения "IT Lab Application".
</span>
</div>
## Structure
```mermaid
graph TD;
agw-->auth;
auth-->auth-db;
auth-db-->auth-db-admin;
```
## Ports
- `8082` - pg Admin
- `3005` - app
## API
- /user
- /devices
- POST
- parameters:
- uuid: string(UUID)
- select?:
- name?: boolean
- fingerprint?: boolean
- isBlocked?: boolean
- return:
- Array:
- name?: string
- fingerprint?: string
- isBlocked?: boolean
- The select statement needs at least one truthy value.

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'auth',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/auth',
};

@ -0,0 +1 @@
migrations

@ -0,0 +1,27 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
uuid String @id @default(uuid()) @db.Uuid
isBlocked Boolean @default(false)
devices UserDevice[] @relation("userDevice")
}
model UserDevice {
fingerprint Bytes @id @db.ByteA
name String @db.VarChar(100)
userUuid String @db.Uuid
isBlocked Boolean @default(false)
user User @relation("userDevice", fields: [userUuid], references: [uuid])
}

@ -0,0 +1,64 @@
{
"name": "auth",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/auth/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/auth",
"main": "apps/auth/src/main.ts",
"tsConfig": "apps/auth/tsconfig.app.json",
"assets": ["apps/auth/src/assets"],
"isolatedConfig": true,
"webpackConfig": "apps/auth/webpack.config.js"
},
"configurations": {
"development": {},
"production": {}
}
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"options": {
"buildTarget": "auth:build"
},
"configurations": {
"development": {
"buildTarget": "auth:build:development"
},
"production": {
"buildTarget": "auth:build:production"
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/auth/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/auth/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}

@ -0,0 +1,225 @@
*.env
.idea
### NotepadPP template
# Notepad++ backups #
*.bak
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# user-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Xcode template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Gcc Patch
/*.gcno
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Kate template
# Swap Files #
.*.kate-swp
.swp.*
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### SublimeText template
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
sftp-config-alt*.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings

@ -0,0 +1,3 @@
POSTGRES_DB=user-authentication
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

@ -0,0 +1,36 @@
version: "3.9"
services:
user-authentication-database:
image: postgres:latest
container_name: user-authentication-database
restart: always
networks:
- user-authentication
volumes:
- user-authentication-database:/var/lib/postgresql
env_file:
- ./database.env
user-authentication-database-admin:
container_name: user-authentication-database-admin
image: bitnami/phppgadmin:latest
restart: always
networks:
- user-authentication
depends_on:
- user-authentication-database
environment:
- DATABASE_HOST=user-authentication-database
ports:
- "8083:8080"
networks:
user-authentication:
name: user-authentication
driver: bridge
volumes:
user-authentication-database:
driver: local
user-authentication-tokens:
driver: local

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let app: TestingModule;
beforeAll(async () => {
app = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
});
describe('getData', () => {
it('should return "Hello API"', () => {
const appController = app.get<AppController>(AppController);
expect(appController.getData()).toEqual({ message: 'Hello API' });
});
});
});

@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getData() {
return this.appService.getData();
}
}

@ -0,0 +1,24 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from "@nestjs/config";
import { AppController } from './app.controller';
import { AppService } from './app.service';
import PortConfig from "../config/port.config";
import KeysConfig from "../config/keys.config";
import { UserModule } from "../user/user.module";
import { DeviceModule } from "../device/device.module";
import { TokenModule } from "../token/token.module";
import { PrismaModule } from "../prisma/prisma.module";
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, load: [PortConfig, KeysConfig] }),
UserModule,
DeviceModule,
TokenModule,
PrismaModule
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { AppService } from './app.service';
describe('AppService', () => {
let service: AppService;
beforeAll(async () => {
const app = await Test.createTestingModule({
providers: [AppService],
}).compile();
service = app.get<AppService>(AppService);
});
describe('getData', () => {
it('should return "Hello API"', () => {
expect(service.getData()).toEqual({ message: 'Hello API' });
});
});
});

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

@ -0,0 +1,10 @@
import { readFileSync } from "fs";
import { join } from "path";
export default () => {
if (process.env.PRIVATE_KEY_PATH === undefined) throw new Error("Incorrect PRIVATE_KEY_PATH format in configurations");
if (process.env.PUBLIC_KEY_PATH === undefined) throw new Error("Incorrect PUBLIC_KEY_PATH format in configurations");
const private_key = readFileSync(join("./", process.env.PRIVATE_KEY_PATH), "utf8");
const public_key = readFileSync(join("./", process.env.PUBLIC_KEY_PATH), "utf8");
return { private_key, public_key };
};

@ -0,0 +1,5 @@
export default () => {
const port = process.env.PORT !== undefined ? parseInt(process.env.PORT, 10) : 80;
if (isNaN(port) || port < 0) throw new Error("Incorrect port format in configurations");
return { port };
};

@ -0,0 +1,2 @@
export const UUIDv4 = /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
export const SHA256 = /^[a-f0-9]{64}$/i;

@ -0,0 +1,84 @@
import { Body, ConflictException, Controller, HttpCode, Post, ValidationPipe } from "@nestjs/common";
import { DeviceService } from "./service/device.service";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { RegisterDto } from "./dto/register.dto";
import { FingerprintDto } from "./dto/fingerprint.dto";
import { UserDto } from "./dto/user.dto";
@Controller("device")
export class DeviceController {
constructor(private readonly device: DeviceService) {
}
@Post("user")
@HttpCode(200)
async User(@Body(new ValidationPipe()) params: UserDto) {
return this.device.User(params);
}
@Post("is-exists")
@HttpCode(200)
async IsExists(@Body(new ValidationPipe()) params: FingerprintDto) {
return await this.device.isExists(params);
}
@Post("register")
@HttpCode(200)
async Register(@Body(new ValidationPipe()) params: RegisterDto) {
try {
await this.device.register(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code === "P2002") throw new ConflictException("device with the same fingerprint already exists");
if (e.code === "P2003") throw new ConflictException("user with this uuid does not exist");
throw e;
}
}
@Post("delete")
@HttpCode(200)
async Delete(@Body(new ValidationPipe()) params: FingerprintDto) {
try {
await this.device.delete(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2025") throw e;
throw new ConflictException("device with this fingerprint does not exist");
}
}
@Post("block")
@HttpCode(200)
async Block(@Body(new ValidationPipe()) params: FingerprintDto) {
try {
await this.device.block(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2025") throw e;
throw new ConflictException("device with this fingerprint does not exist");
}
}
@Post("unblock")
@HttpCode(200)
async Unblock(@Body(new ValidationPipe()) params: FingerprintDto) {
try {
await this.device.unblock(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2025") throw e;
throw new ConflictException("device with this fingerprint does not exist");
}
}
@Post("is-blocked")
@HttpCode(200)
async IsBlocked(@Body(new ValidationPipe()) params: FingerprintDto) {
const isBlocked = await this.device.isBlocked(params);
if (isBlocked !== null) return isBlocked.isBlocked;
throw new ConflictException("device with this fingerprint does not exist");
}
}

@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { DeviceController } from "./device.controller";
import { DeviceService } from "./service/device.service";
@Module({
providers: [DeviceService],
controllers: [DeviceController],
exports: [DeviceService]
})
export class DeviceModule {
}

@ -0,0 +1,15 @@
import { IsBoolean, IsOptional } from "class-validator";
export class UserSelectDto {
@IsOptional()
@IsBoolean()
public uuid?: boolean | null;
@IsOptional()
@IsBoolean()
public isBlocked?: boolean | null;
constructor(uuid: boolean, isBlocked: boolean) {
this.uuid = uuid;
this.isBlocked = isBlocked;
}
}

@ -0,0 +1,11 @@
import { IsHexadecimal, Length } from "class-validator";
export class FingerprintDto {
@IsHexadecimal()
@Length(64, 64)
public fingerprint: string;
constructor(fingerprint: string) {
this.fingerprint = fingerprint;
}
}

@ -0,0 +1,18 @@
import { IsHexadecimal, IsString, IsUUID, Length } from "class-validator";
export class RegisterDto {
@IsHexadecimal()
@Length(64, 64)
public fingerprint: string;
@IsString()
@Length(0, 100)
public name: string;
@IsUUID(4)
public userUuid: string;
constructor(fingerprint: string, name: string, userUuid: string) {
this.fingerprint = fingerprint;
this.name = name;
this.userUuid = userUuid;
}
}

@ -0,0 +1,20 @@
import { IsHexadecimal, IsOptional, Length, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { UserSelectDto } from "./elements/user-select.dto";
export class UserDto {
@IsHexadecimal()
@Length(64, 64)
public fingerprint: string;
@IsOptional()
@ValidateNested({ each: false })
@Type(() => UserSelectDto)
public select?: UserSelectDto | null;
constructor(fingerprint: string, select?: UserSelectDto | null) {
this.fingerprint = fingerprint;
this.select = select;
}
}

@ -0,0 +1,64 @@
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
import { PrismaService } from "../../Prisma/service/prisma.service";
import { UserDto } from "../dto/user.dto";
import { PrismaClientValidationError } from "@prisma/client/runtime";
@Injectable()
export class DeviceService {
constructor(private readonly prisma: PrismaService) {
}
public getUserUuidAsPrismaPromise(params: { fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
return this.prisma.userDevice.findUnique({ where: { fingerprint }, select: { userUuid: true } });
}
public async User(params: UserDto) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
let select: { uuid?: boolean, isBlocked?: boolean } | undefined = { uuid: true, isBlocked: true };
if (params.select !== undefined && params.select !== null) {
select = {};
if (params.select.uuid !== undefined && params.select.uuid !== null) select.uuid = params.select.uuid;
if (params.select.isBlocked !== undefined && params.select.isBlocked !== null) select.isBlocked = params.select.isBlocked;
}
try {
const res: { user: { uuid?: string, isBlocked?: boolean } } | null =
await this.prisma.userDevice.findUnique({ where: { fingerprint }, select: { user: { select } } });
if (res === null) throw new ConflictException("device with this fingerprint does not exist");
return res.user;
} catch (e) {
if (!(e instanceof PrismaClientValidationError)) throw e;
throw new BadRequestException("The select statement needs at least one truthy value.");
}
}
public async isExists(params: { fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
return (await this.prisma.userDevice.findUnique({ where: { fingerprint }, select: null }) !== null);
}
public register(params: { userUuid: string, name: string, fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
return this.prisma.userDevice.create({ data: { ...params, fingerprint }, select: null });
}
public async delete(params: { fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
await this.prisma.userDevice.delete({ where: { fingerprint }, select: null });
}
public async block(params: { fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
await this.prisma.userDevice.update({ where: { fingerprint }, data: { isBlocked: true }, select: null });
}
public async unblock(params: { fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
await this.prisma.userDevice.update({ where: { fingerprint }, data: { isBlocked: false }, select: null });
}
public isBlocked(params: { fingerprint: string }) {
const fingerprint = Buffer.from(params.fingerprint, "hex");
return this.prisma.userDevice.findUnique({ where: { fingerprint }, select: { isBlocked: true } });
}
}

@ -0,0 +1,22 @@
/**
* This is not a production server yet!
* This is only a minimal backend to get started.
*/
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3005;
await app.listen(port);
Logger.log(
`🚀 Application is running on: http://localhost:${port}/${globalPrefix}`
);
}
bootstrap();

@ -0,0 +1 @@
export class RecordNotFoundError extends Error {}

@ -0,0 +1 @@
export class UniqueConstraintFailedError extends Error {}

@ -0,0 +1,26 @@
import {
CallHandler,
ConflictException,
ExecutionContext,
Injectable,
NestInterceptor,
NotFoundException
} from "@nestjs/common";
import { catchError, Observable } from "rxjs";
import { RecordNotFoundError } from "../error/record-not-found.error";
import { UniqueConstraintFailedError } from "../error/unique-constraint-failed.error";
@Injectable()
export class PrismaErrorInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => {
if (err instanceof RecordNotFoundError) throw new NotFoundException(err.message);
if (err instanceof UniqueConstraintFailedError) throw new ConflictException(err.message);
throw err;
})
);
}
}

@ -0,0 +1,10 @@
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./service/prisma.service";
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService]
})
export class PrismaModule {
}

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

@ -0,0 +1,15 @@
import { INestApplication, Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on("beforeExit", async () => {
await app.close();
});
}
}

@ -0,0 +1,19 @@
import { IsHexadecimal, IsOptional, IsString, IsUUID, Length } from "class-validator";
export class GenerateRefreshDto {
@IsUUID(4)
public user_uuid: string;
@IsHexadecimal()
@Length(64, 64)
public device_fingerprint: string;
@IsOptional()
@IsString()
@Length(0, 100)
public device_name?: string | null;
constructor(user_uuid: string, device_fingerprint: string, device_name?: string) {
this.user_uuid = user_uuid;
this.device_fingerprint = device_fingerprint;
this.device_name = device_name;
}
}

@ -0,0 +1,10 @@
import { IsString } from "class-validator";
export class RefreshTokenDto {
@IsString()
public refresh_token: string;
constructor(refresh_token: string) {
this.refresh_token = refresh_token;
}
}

@ -0,0 +1,122 @@
import { BadRequestException, ConflictException, Injectable, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { UserService } from "../../user/service/user.service";
import { DeviceService } from "../../device/service/device.service";
import { PrismaService } from "../../Prisma/service/prisma.service";
import { PrismaPromise } from "@prisma/client";
import { SHA256, UUIDv4 } from "../../const/regex";
@Injectable()
export class TokenService {
constructor(private readonly jwt: JwtService,
private readonly user: UserService,
private readonly device: DeviceService,
private readonly prisma: PrismaService) {
}
public async CheckRefresh(params: { refresh_token: string }) {
try {
await this.CheckRefreshTokenAssert(params);
return true;
} catch (e) {
return false;
}
}
public async GenerateAccess(params: { refresh_token: string }) {
const data = await this.CheckRefreshTokenAssert(params);
return await this.jwt.signAsync({
type: "access",
user_uuid: data.user_uuid,
device_fingerprint: data.device_fingerprint
}, { expiresIn: 600 });
}
public async GenerateRefresh(params: { user_uuid: string, device_fingerprint: string, device_name?: string | null }) {
const { is_user_blocked, is_device_blocked } = await this.GenerateRefreshBlocksChecker(params);
if (is_user_blocked) throw new ConflictException("The user is blocked");
if (is_device_blocked) throw new ConflictException("The device is blocked");
return await this.jwt.signAsync({
type: "refresh",
user_uuid: params.user_uuid,
device_fingerprint: params.device_fingerprint
}, {
expiresIn: "7d"
});
}
private async JwtVerifyGuard<T extends object = any>(params: { jwt_token: string }) {
try {
return await this.jwt.verifyAsync<T>(params.jwt_token);
} catch (e) {
throw new UnauthorizedException("Invalid token");
}
}
private async IsUserAndDeviceBlockedAssert(params: { user_uuid: string, device_fingerprint: string }) {
const res = await this.prisma.$transaction([
this.user.isBlocked({ uuid: params.user_uuid }),
this.device.isBlocked({ fingerprint: params.device_fingerprint }),
this.device.getUserUuidAsPrismaPromise({ fingerprint: params.device_fingerprint })
]);
if (res[0] === null) throw new ConflictException("user with this uuid does not exist");
if (res[1] === null || res[2] === null) throw new ConflictException("device with this fingerprint does not exist");
if (res[0].isBlocked) throw new ConflictException("The user is blocked");
if (res[1].isBlocked) throw new ConflictException("The device is blocked");
if (res[2].userUuid !== params.user_uuid) throw new ConflictException("The device is connected to another user");
}
private async CheckRefreshTokenAssert(params: { refresh_token: string }) {
const data = await this.JwtVerifyGuard<{ user_uuid: string, device_fingerprint: string, type: string }>({ jwt_token: params.refresh_token });
if (typeof data !== "object") throw new BadRequestException("JWT payload does not object");
if (typeof data.user_uuid !== "string") throw new BadRequestException("user_uuid field does not string");
if (!UUIDv4.test(data.user_uuid)) throw new BadRequestException("user_uuid field does not uuid");
if (typeof data.device_fingerprint !== "string") throw new BadRequestException("device_fingerprint field does not string");
if (!SHA256.test(data.device_fingerprint)) throw new BadRequestException("device_fingerprint field does not sha256");
if (typeof data.type !== "string") throw new BadRequestException("type field does not string");
if (data.type !== "refresh") throw new BadRequestException("type field does not equal \"refresh\" value");
await this.IsUserAndDeviceBlockedAssert(data);
return data;
}
private async GenerateRefreshBlocksChecker(params: { user_uuid: string, device_fingerprint: string, device_name?: string | null }) {
const transaction: PrismaPromise<any>[] = [];
const user_exists = await this.user.isExists({ uuid: params.user_uuid });
const device_exists = await this.device.isExists({ fingerprint: params.device_fingerprint });
if (user_exists) {
if (device_exists) {
if ((await this.device.getUserUuidAsPrismaPromise({ fingerprint: params.device_fingerprint }))?.userUuid !== params.user_uuid)
throw new ConflictException("The device is connected to another user");
} else {
if (params.device_name === null || params.device_name === undefined)
throw new ConflictException("The device does not exist, but the new device name is not specified");
transaction.push(
this.device.register({
userUuid: params.user_uuid,
fingerprint: params.device_fingerprint,
name: params.device_name
}));
}
} else {
if (device_exists)
throw new ConflictException("The device is connected to another user");
if (params.device_name === null || params.device_name === undefined)
throw new ConflictException("The device does not exist, but the new device name is not specified");
transaction.push(
this.user.register({ uuid: params.user_uuid }),
this.device.register({
userUuid: params.user_uuid,
fingerprint: params.device_fingerprint,
name: params.device_name
}));
}
transaction.push(
this.user.isBlocked({ uuid: params.user_uuid }),
this.device.isBlocked({ fingerprint: params.device_fingerprint })
);
const transaction_res = await this.prisma.$transaction(transaction);
const is_user_blocked = transaction_res[transaction_res.length - 2]?.isBlocked as boolean | undefined;
const is_device_blocked = transaction_res[transaction_res.length - 1]?.isBlocked as boolean | undefined;
return { is_user_blocked, is_device_blocked };
}
}

@ -0,0 +1,36 @@
import { Body, Controller, Get, HttpCode, Post, ValidationPipe } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { TokenService } from "./service/token.service";
import { GenerateRefreshDto } from "./dto/generate-refresh.dto";
import { RefreshTokenDto } from "./dto/refresh-token.dto";
@Controller("token")
export class TokenController {
constructor(private config: ConfigService<{ port: number, private_key: string, public_key: string }>,
private token: TokenService) {
}
@Post("generate/refresh")
@HttpCode(200)
GenerateRefresh(@Body(new ValidationPipe()) params: GenerateRefreshDto) {
return this.token.GenerateRefresh(params);
}
@Post("generate/access")
@HttpCode(200)
GenerateAccess(@Body(new ValidationPipe()) params: RefreshTokenDto) {
return this.token.GenerateAccess(params);
}
@Post("is-valid/refresh")
@HttpCode(200)
CheckRefresh(@Body(new ValidationPipe()) params: RefreshTokenDto) {
return this.token.CheckRefresh(params);
}
@Get("public-key")
@HttpCode(200)
async GetAccessPublicKey() {
return this.config.get<string>("public_key");
}
}

@ -0,0 +1,26 @@
import { Module } from "@nestjs/common";
import { TokenController } from "./token.controller";
import { JwtModule } from "@nestjs/jwt";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TokenService } from "./service/token.service";
import { UserModule } from "../user/user.module";
import { DeviceModule } from "../device/device.module";
@Module({
imports: [
UserModule,
DeviceModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService<{ port: number, private_key: string, public_key: string }>) => ({
publicKey: configService.get<string>("public_key"),
privateKey: configService.get<string>("private_key"),
signOptions: { algorithm: "RS512", issuer: "sc-user-authentication" }
})
})],
controllers: [TokenController],
providers: [TokenService]
})
export class TokenModule {
}

@ -0,0 +1,17 @@
import { IsOptional, IsUUID, ValidateNested } from "class-validator";
import { Type } from "class-transformer";
import { DeviceSelectDto } from "./elements/device-select.dto";
export class DevicesDto {
@IsUUID(4)
public uuid: string;
@IsOptional()
@ValidateNested({ each: false })
@Type(() => DeviceSelectDto)
public select?: DeviceSelectDto | null;
constructor(uuid: string, select: DeviceSelectDto) {
this.uuid = uuid;
this.select = select;
}
}

@ -0,0 +1,19 @@
import { IsBoolean, IsOptional } from "class-validator";
export class DeviceSelectDto {
@IsOptional()
@IsBoolean()
public name?: boolean | null;
@IsOptional()
@IsBoolean()
public fingerprint?: boolean | null;
@IsOptional()
@IsBoolean()
public isBlocked?: boolean | null;
constructor(name: boolean, fingerprint: boolean, isBlocked: boolean) {
this.name = name;
this.fingerprint = fingerprint;
this.isBlocked = isBlocked;
}
}

@ -0,0 +1,10 @@
import { IsUUID } from "class-validator";
export class UuidDto {
@IsUUID(4)
public uuid: string;
constructor(uuid: string) {
this.uuid = uuid;
}
}

@ -0,0 +1,54 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { PrismaService } from "../../Prisma/service/prisma.service";
import { DevicesDto } from "../dto/devices.dto";
import { PrismaClientValidationError } from "@prisma/client/runtime";
@Injectable()
export class UserService {
constructor(private readonly prisma: PrismaService) {
}
public async devices(params: DevicesDto): Promise<{ name?: string, fingerprint?: string, isBlocked?: boolean }[]> {
let select: { name?: boolean, fingerprint?: boolean, isBlocked?: boolean } | undefined = {
name: true, fingerprint: true, isBlocked: true
};
if (params.select !== undefined && params.select !== null) {
select = {};
if (params.select.name !== undefined && params.select.name !== null) select.name = params.select.name;
if (params.select.fingerprint !== undefined && params.select.fingerprint !== null) select.fingerprint = params.select.fingerprint;
if (params.select.isBlocked !== undefined && params.select.isBlocked !== null) select.isBlocked = params.select.isBlocked;
}
try {
const res: { name?: string, isBlocked?: boolean, fingerprint?: Buffer }[] =
await this.prisma.userDevice.findMany({ where: { userUuid: params.uuid }, select });
return res.map(value => ({ ...value, fingerprint: value.fingerprint?.toString("hex") }));
} catch (e) {
if (!(e instanceof PrismaClientValidationError)) throw e;
throw new BadRequestException("The select statement needs at least one truthy value.");
}
}
public async isExists(params: { uuid: string }) {
return (await this.prisma.user.findUnique({ where: params, select: null }) !== null);
}
public register(params: { uuid: string }) {
return this.prisma.user.create({ data: params, select: null });
}
public async delete(params: { uuid: string }) {
await this.prisma.user.delete({ where: params, select: null });
}
public async block(params: { uuid: string }) {
await this.prisma.user.update({ where: params, data: { isBlocked: true }, select: null });
}
public async unblock(params: { uuid: string }) {
await this.prisma.user.update({ where: params, data: { isBlocked: false }, select: null });
}
public isBlocked(params: { uuid: string }) {
return this.prisma.user.findUnique({ where: params, select: { isBlocked: true } });
}
}

@ -0,0 +1,80 @@
import { Body, ConflictException, Controller, HttpCode, Post, ValidationPipe } from "@nestjs/common";
import { UserService } from "./service/user.service";
import { UuidDto } from "./dto/uuid.dto";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { DevicesDto } from "./dto/devices.dto";
@Controller("user")
export class UserController {
constructor(private readonly user: UserService) {
}
@Post("devices")
@HttpCode(200)
async Devices(@Body(new ValidationPipe()) params: DevicesDto) {
if (await this.user.isExists({ uuid: params.uuid })) return await this.user.devices(params);
else throw new ConflictException("user with this uuid does not exist");
}
@Post("is-exists")
@HttpCode(200)
async IsExists(@Body(new ValidationPipe()) params: UuidDto) {
return await this.user.isExists(params);
}
@Post("register")
@HttpCode(200)
async Register(@Body(new ValidationPipe()) params: UuidDto) {
try {
await this.user.register(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2002") throw e;
throw new ConflictException("user with the same uuid already exists");
}
}
@Post("delete")
@HttpCode(200)
async Delete(@Body(new ValidationPipe()) params: UuidDto) {
try {
await this.user.delete(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2025") throw e;
throw new ConflictException("user with this uuid does not exist");
}
}
@Post("block")
@HttpCode(200)
async Block(@Body(new ValidationPipe()) params: UuidDto) {
try {
await this.user.block(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2025") throw e;
throw new ConflictException("user with this uuid does not exist");
}
}
@Post("unblock")
@HttpCode(200)
async Unblock(@Body(new ValidationPipe()) params: UuidDto) {
try {
await this.user.unblock(params);
} catch (e) {
if (!(e instanceof PrismaClientKnownRequestError)) throw e;
if (e.code !== "P2025") throw e;
throw new ConflictException("user with this uuid does not exist");
}
}
@Post("is-blocked")
@HttpCode(200)
async IsBlocked(@Body(new ValidationPipe()) params: UuidDto) {
const isBlocked = await this.user.isBlocked(params);
if (isBlocked !== null) return isBlocked.isBlocked;
throw new ConflictException("user with this uuid does not exist");
}
}

@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { UserController } from "./user.controller";
import { UserService } from "./service/user.service";
@Module({
providers: [UserService],
controllers: [UserController],
exports: [UserService]
})
export class UserModule {
}

@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["node"],
"emitDecoratorMetadata": true,
"target": "es2021"
},
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}

@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

@ -0,0 +1,8 @@
const { composePlugins, withNx } = require('@nx/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

@ -0,0 +1,10 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
}
]
}

@ -0,0 +1,19 @@
/* eslint-disable */
export default {
displayName: 'user-data-e2e',
preset: '../../jest.preset.js',
globalSetup: '<rootDir>/src/support/global-setup.ts',
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
setupFiles: ['<rootDir>/src/support/test-setup.ts'],
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
},
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/user-data-e2e',
};

@ -0,0 +1,22 @@
{
"name": "user-data-e2e",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"implicitDependencies": ["user-data"],
"targets": {
"e2e": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{e2eProjectRoot}"],
"options": {
"jestConfig": "apps/user-data-e2e/jest.config.ts",
"passWithNoTests": true
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/user-data-e2e/**/*.{js,ts}"]
}
}
}
}

@ -0,0 +1,10 @@
/* eslint-disable */
var __TEARDOWN_MESSAGE__: string;
module.exports = async function () {
// Start services that that the app needs to run (e.g. database, docker-compose, etc.).
console.log('\nSetting up...\n');
// Hint: Use `globalThis` to pass variables to global teardown.
globalThis.__TEARDOWN_MESSAGE__ = '\nTearing down...\n';
};

@ -0,0 +1,7 @@
/* eslint-disable */
module.exports = async function () {
// Put clean up logic here (e.g. stopping services, docker-compose, etc.).
// Hint: `globalThis` is shared between setup and teardown.
console.log(globalThis.__TEARDOWN_MESSAGE__);
};

@ -0,0 +1,10 @@
/* eslint-disable */
import axios from 'axios';
module.exports = async function () {
// Configure axios for tests to use.
const host = process.env.HOST ?? 'localhost';
const port = process.env.PORT ?? '3000';
axios.defaults.baseURL = `http://${host}:${port}`;
};

@ -0,0 +1,10 @@
import axios from 'axios';
describe('GET /api', () => {
it('should return a message', async () => {
const res = await axios.get(`/api`);
expect(res.status).toBe(200);
expect(res.data).toEqual({ message: 'Hello API' });
});
});

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.spec.json"
}
],
"compilerOptions": {
"esModuleInterop": true
}
}

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.ts"]
}

@ -0,0 +1,13 @@
$$
@version: 0.1.0;
@scuuid: 27895030-15ec-11ee-be56-0242ac120002;
@type: service;
@platform: nestjs;
@license: BSD-3-Clause;
@owner: artem-darius weber;
@author: ;
@title: user-data;
@desc: ;
@rp: kubsu it lab;
@vr: 7093;
$$

@ -0,0 +1,8 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL=postgresql://postgres:postgres@sc-user-data-database/sc-user-data
PORT=3002

@ -0,0 +1,18 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

@ -0,0 +1,3 @@
node_modules
# Keep environment variables out of version control
.env

@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1
FROM node:18
WORKDIR /app
COPY --chown=node:node . .
RUN rm -f *.env *.env.*
RUN apt-get update -y && apt-get install -y dumb-init
RUN npm install
ENV NODE_ENV=production
RUN npm run prefullbuild && npm run prebuild && npm run build
RUN mkdir temp temp/.prisma temp/@prisma temp/prisma && cp -r ./node_modules/.prisma/* ./temp/.prisma/ && cp -r ./node_modules/@prisma/* ./temp/@prisma/ && cp -r ./node_modules/prisma/* ./temp/prisma/
RUN rm -rdf node_modules
RUN npm install --production
RUN cp -r ./temp/* ./node_modules/ && rm -rdf temp
RUN ls | grep -v node_modules | grep -v dist | xargs rm -rfv
RUN cp -r ./dist/* ./ && rm -rdf dist
USER node
CMD ["dumb-init", "node", "./main.js"]

@ -0,0 +1,11 @@
Copyright 2023 SC (DJEEFT) ©
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,23 @@
<div style="display: flex; flex-direction: row; align-items: center; justify-content: center;">
<img style="border-radius: 1em; align-self: center;" src="https://drive.google.com/uc?export=view&id=1_xQWdF3RtL1MRcdDESMPXMBiAQDHCDGT" width="45">
<h1 style="text-align: center; margin-left: 10px; margin-top: 2px;">IT Lab Platform - User Data Service</h1>
</div>
<div style="display: flex; align-items: center; border: 1px solid #ccc; padding: 10px; border-radius: 5px;">
<span style="font-size: 20px; margin-right: 10px;"></span>
<span style="font-size: 16px;">
Эта директория содержит исходный код сервиса базовых пользовательских данных приложения "IT Lab Application".
</span>
</div>
## Ports
- `8083` - pg Admin
- `3002` - app
## Structure
```mermaid
graph TD;
agw-->user-data;
user-data-->user-data-db;
user-data-db-->user-data-pg-admin;
```

@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'user-data',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/user-data',
};

@ -0,0 +1 @@
migrations

@ -0,0 +1,53 @@
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
uuid String @id @default(uuid()) @db.Uuid
email Email? @relation("mainEmail")
phoneNumber PhoneNumber? @relation("mainPhoneNumber")
reserveEmails Email[] @relation("reserveEmails")
reservePhoneNumbers PhoneNumber[] @relation("reservePhoneNumbers")
extendedData Json @default("{}") @db.JsonB
createdAt DateTime @db.Timestamp(3)
updatedAt DateTime @db.Timestamp(3)
}
model Email {
uuid String @id @default(uuid()) @db.Uuid
email String @unique @db.VarChar(256)
userUuid String? @unique @db.Uuid
user User? @relation("mainEmail", fields: [userUuid], references: [uuid])
userUuidReserve String? @db.Uuid
userReserve User? @relation("reserveEmails", fields: [userUuidReserve], references: [uuid])
createdAt DateTime @db.Timestamp(3)
updatedAt DateTime @db.Timestamp(3)
}
model PhoneNumber {
uuid String @id @default(uuid()) @db.Uuid
phoneNumber String @unique @db.VarChar(20)
userUuid String? @unique @db.Uuid
user User? @relation("mainPhoneNumber", fields: [userUuid], references: [uuid])
userUuidReserve String? @db.Uuid
userReserve User? @relation("reservePhoneNumbers", fields: [userUuidReserve], references: [uuid])
createdAt DateTime @db.Timestamp(3)
updatedAt DateTime @db.Timestamp(3)
}

@ -0,0 +1,64 @@
{
"name": "user-data",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/user-data/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/user-data",
"main": "apps/user-data/src/main.ts",
"tsConfig": "apps/user-data/tsconfig.app.json",
"assets": ["apps/user-data/src/assets"],
"isolatedConfig": true,
"webpackConfig": "apps/user-data/webpack.config.js"
},
"configurations": {
"development": {},
"production": {}
}
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",
"options": {
"buildTarget": "user-data:build"
},
"configurations": {
"development": {
"buildTarget": "user-data:build:development"
},
"production": {
"buildTarget": "user-data:build:production"
}
}
},
"lint": {
"executor": "@nx/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/user-data/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/user-data/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
}
},
"tags": []
}

@ -0,0 +1,225 @@
*.env
.idea
### NotepadPP template
# Notepad++ backups #
*.bak
### VisualStudioCode template
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# user-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Xcode template
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
## Gcc Patch
/*.gcno
### Linux template
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
### Kate template
# Swap Files #
.*.kate-swp
.swp.*
### Windows template
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
### macOS template
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### SublimeText template
# Cache files for Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# Workspace files are user-specific
*.sublime-workspace
# Project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using Sublime Text
# *.sublime-project
# SFTP configuration file
sftp-config.json
sftp-config-alt*.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings

@ -0,0 +1,3 @@
POSTGRES_DB=sc-user-data
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres

@ -0,0 +1,34 @@
version: "3.9"
services:
user-data-database:
image: postgres:latest
container_name: user-data-database
restart: always
networks:
- user-data
volumes:
- user-data-database:/var/lib/postgresql
env_file:
- ./database.env
user-data-database-admin:
container_name: user-data-database-admin
image: bitnami/phppgadmin:latest
restart: always
networks:
- user-data
depends_on:
- user-data-database
environment:
- DATABASE_HOST=user-data-database
ports:
- "8083:8080"
networks:
user-data:
name: user-data
driver: bridge
volumes:
user-data-database:
driver: local

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './service/app.service';
describe('AppController', () => {
let app: TestingModule;
beforeAll(async () => {
app = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
});
describe('getData', () => {
it('should return "Hello API"', () => {
const appController = app.get<AppController>(AppController);
expect(appController.getData()).toEqual({ message: 'Hello API' });
});
});
});

@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './service/app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getData() {
return this.appService.getData();
}
}

@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './service/app.service';
import { UserModule } from "../user/user.module";
import { EmailModule } from "../email/email.module";
import { PhoneNumberModule } from "../phone-number/phone-number.module";
@Module({
imports: [UserModule, EmailModule, PhoneNumberModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

@ -0,0 +1,21 @@
import { Test } from '@nestjs/testing';
import { AppService } from './app.service';
describe('AppService', () => {
let service: AppService;
beforeAll(async () => {
const app = await Test.createTestingModule({
providers: [AppService],
}).compile();
service = app.get<AppService>(AppService);
});
describe('getData', () => {
it('should return "Hello API"', () => {
expect(service.getData()).toEqual({ message: 'Hello API' });
});
});
});

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

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

@ -0,0 +1,53 @@
import { Body, Controller, HttpCode, Inject, Post, UseInterceptors } from "@nestjs/common";
import { Prisma, Email } from "@prisma/client";
import { EmailService } from "./service/email.service";
import { PrismaErrorInterceptor } from "../prisma/interceptor/prisma-error.interceptor";
@Controller("email")
@UseInterceptors(PrismaErrorInterceptor)
export class EmailController {
constructor(@Inject(EmailService) private email: EmailService) {
}
@Post("find/unique")
@HttpCode(200)
async FindUnique(@Body() emailFindUniqueArgs: Prisma.EmailFindUniqueArgs): Promise<Partial<Email> | null> {
return await this.email.FindUnique(emailFindUniqueArgs);
}
@Post("find/first")
@HttpCode(200)
async FindFirst(@Body() emailFindFirstArgs: Prisma.EmailFindFirstArgs): Promise<Partial<Email> | null> {
return await this.email.FindFirst(emailFindFirstArgs);
}
@Post("find/many")
@HttpCode(200)
async FindMany(@Body() emailFindManyArgs: Prisma.EmailFindManyArgs): Promise<Partial<Email>[]> {
return await this.email.FindMany(emailFindManyArgs);
}
@Post("update")
@HttpCode(200)
async Update(@Body() emailUpdateArgs: Prisma.EmailUpdateArgs): Promise<Partial<Email>> {
return await this.email.Update(emailUpdateArgs);
}
@Post("count")
@HttpCode(200)
async Count(@Body() emailCountArgs: Prisma.EmailCountArgs): Promise<number> {
return await this.email.Count(emailCountArgs);
}
@Post("create")
@HttpCode(200)
async Create(@Body() emailCreateArgs: Prisma.EmailCreateArgs): Promise<Partial<Email>> {
return await this.email.Create(emailCreateArgs);
}
@Post("delete")
@HttpCode(200)
async Delete(@Body() emailDeleteArgs: Prisma.EmailDeleteArgs): Promise<Partial<Email>> {
return await this.email.Delete(emailDeleteArgs);
}
}

@ -0,0 +1,12 @@
import { Module } from "@nestjs/common";
import { EmailController } from "./email.controller";
import { EmailService } from "./service/email.service";
import { PrismaModule } from "../prisma/prisma.module";
@Module({
imports: [PrismaModule],
controllers: [EmailController],
providers: [EmailService]
})
export class EmailModule {
}

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

@ -0,0 +1,62 @@
import { Inject, Injectable } from "@nestjs/common";
import { PrismaService } from '../../prisma/service/prisma.service';
import { Email, Prisma } from "@prisma/client";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import { UniqueConstraintFailedError } from "../../prisma/error/unique-constraint-failed.error";
import { RecordNotFoundError } from "../../prisma/error/record-not-found.error";
@Injectable()
export class EmailService {
constructor(@Inject(PrismaService) private prisma: PrismaService) {
}
async FindUnique(emailFindUniqueArgs: Prisma.EmailFindUniqueArgs): Promise<Partial<Email> | null> {
return await this.prisma.email.findUnique(emailFindUniqueArgs);
}
async FindFirst(emailFindFirstArgs: Prisma.EmailFindFirstArgs): Promise<Partial<Email> | null> {
return await this.prisma.email.findFirst(emailFindFirstArgs);
}
async FindMany(emailFindManyArgs: Prisma.EmailFindManyArgs): Promise<Partial<Email>[]> {
return await this.prisma.email.findMany(emailFindManyArgs);
}
async Update(emailUpdateArgs: Prisma.EmailUpdateArgs): Promise<Partial<Email>> {
try {
return await this.prisma.email.update(emailUpdateArgs);
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2025") throw new RecordNotFoundError("Record to update not found.");
if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed");
}
throw e;
}
}
async Count(emailCountArgs: Prisma.EmailCountArgs): Promise<number> {
return await this.prisma.email.count(emailCountArgs);
}
async Create(emailCreateArgs: Prisma.EmailCreateArgs): Promise<Partial<Email>> {
try {
return await this.prisma.email.create(emailCreateArgs);
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2002") throw new UniqueConstraintFailedError("Unique constant failed");
}
throw e;
}
}
async Delete(emailDeleteArgs: Prisma.EmailDeleteArgs): Promise<Partial<Email>> {
try {
return await this.prisma.email.delete(emailDeleteArgs);
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2025") throw new RecordNotFoundError("Record to delete not found.");
}
throw e;
}
}
}

@ -0,0 +1,22 @@
/**
* This is not a production server yet!
* This is only a minimal backend to get started.
*/
import { Logger } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const globalPrefix = 'api';
app.setGlobalPrefix(globalPrefix);
const port = process.env.PORT || 3002;
await app.listen(port);
Logger.log(
`🚀 User Data Application is running on: http://localhost:${port}/${globalPrefix}`
);
}
bootstrap();

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

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

Loading…
Cancel
Save