initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
*.md
|
||||||
|
fixed-smart.db
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
name: Build and Publish Organization Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: docker
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: gitea.jzitnik.dev
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.TOKEN }}
|
||||||
|
|
||||||
|
- name: Convert Owner and Repo to Lowercase
|
||||||
|
id: string_prep
|
||||||
|
run: |
|
||||||
|
FULL_REPO="${GITHUB_REPOSITORY,,}"
|
||||||
|
OWNER="${FULL_REPO%%/*}"
|
||||||
|
REPO="${FULL_REPO#*/}"
|
||||||
|
|
||||||
|
echo "owner=$OWNER" >> $GITHUB_OUTPUT
|
||||||
|
echo "repo=$REPO" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Extract Metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: gitea.jzitnik.dev/${{ steps.string_prep.outputs.owner }}/${{ steps.string_prep.outputs.repo }}
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=sha,format=short
|
||||||
|
|
||||||
|
- name: Build and Push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
data/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS run
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache dumb-init
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV DB_PATH=/app/data/fixed-smart.db
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["dumb-init", "node", "dist/main.js"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Fixed Smart Backend
|
||||||
|
|
||||||
|
Implementace vibe-coded backendu aplikace Fixed Smart
|
||||||
|
|
||||||
|
Toto je vibe-coded backend implementovaný z ručně napsané API specifikace z Smali instrukcí z APK souboru.
|
||||||
|
|
||||||
|
NOTE: Některé nedůležité funkce nejsou implementované (OAuth login, Analytika, atd.)
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Docker compose:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
fixed-api:
|
||||||
|
## TODO: Add url for image
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- REGISTRATION_DISABLED=false # Disabling registration
|
||||||
|
volumes:
|
||||||
|
- fixed-data:/app/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fixed-data:
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vibe-coded
|
||||||
|
|
||||||
|
Protože kód je 100% od LLM, nezaručuji, že bude fungovat, bude bezpečný nebo cokoli mezi tím.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+11234
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
|||||||
|
{
|
||||||
|
"name": "api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"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": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/jwt": "^11.0.2",
|
||||||
|
"@nestjs/passport": "^11.0.5",
|
||||||
|
"@nestjs/platform-express": "^11.1.28",
|
||||||
|
"@nestjs/typeorm": "^11.0.3",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"better-sqlite3": "^12.11.1",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.15.1",
|
||||||
|
"multer": "^2.2.0",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sqlite3": "^6.0.1",
|
||||||
|
"typeorm": "^1.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^24.0.0",
|
||||||
|
"@types/supertest": "^7.0.0",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^17.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
|
"allowScripts": {
|
||||||
|
"bcrypt@6.0.0": true,
|
||||||
|
"sqlite3@6.0.1": true,
|
||||||
|
"better-sqlite3@12.11.1": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { APP_GUARD, APP_FILTER } from '@nestjs/core';
|
||||||
|
import { JwtAuthGuard } from './common/guards/jwt-auth.guard';
|
||||||
|
import { AllExceptionsFilter } from './common/filters/http-exception.filter';
|
||||||
|
import { AuthModule } from './auth/auth.module';
|
||||||
|
import { UsersModule } from './users/users.module';
|
||||||
|
import { DevicesModule } from './devices/devices.module';
|
||||||
|
import { EventsModule } from './events/events.module';
|
||||||
|
import { SafeZonesModule } from './safe-zones/safe-zones.module';
|
||||||
|
import { SharingModule } from './sharing/sharing.module';
|
||||||
|
import { LimitsModule } from './limits/limits.module';
|
||||||
|
import { PushModule } from './push/push.module';
|
||||||
|
import { NightModeModule } from './night-mode/night-mode.module';
|
||||||
|
import { GeolocationModule } from './geolocation/geolocation.module';
|
||||||
|
import { RouteModule } from './route/route.module';
|
||||||
|
import { FaqModule } from './faq/faq.module';
|
||||||
|
import { MiscModule } from './misc/misc.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'better-sqlite3',
|
||||||
|
database: process.env.DB_PATH || 'fixed-smart.db',
|
||||||
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
|
synchronize: true,
|
||||||
|
}),
|
||||||
|
AuthModule,
|
||||||
|
UsersModule,
|
||||||
|
DevicesModule,
|
||||||
|
EventsModule,
|
||||||
|
SafeZonesModule,
|
||||||
|
SharingModule,
|
||||||
|
LimitsModule,
|
||||||
|
PushModule,
|
||||||
|
NightModeModule,
|
||||||
|
GeolocationModule,
|
||||||
|
RouteModule,
|
||||||
|
FaqModule,
|
||||||
|
MiscModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: APP_GUARD, useClass: JwtAuthGuard },
|
||||||
|
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { EmailDto } from './dto/email.dto';
|
||||||
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('user/register')
|
||||||
|
async register(@Body() dto: RegisterDto) {
|
||||||
|
await this.authService.register(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('user/login')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
async login(@Body() dto: LoginDto) {
|
||||||
|
return this.authService.login(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('user/exists')
|
||||||
|
async exists(@Body() dto: EmailDto) {
|
||||||
|
return this.authService.checkExists(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Post('user/forgot-password')
|
||||||
|
async forgotPassword(@Body() dto: EmailDto) {
|
||||||
|
await this.authService.forgotPassword(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { JwtStrategy } from './jwt.strategy';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([User]),
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.register({
|
||||||
|
secret: process.env.JWT_SECRET || 'fixed-smart-secret-key',
|
||||||
|
signOptions: { expiresIn: '365d' },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
ConflictException,
|
||||||
|
UnauthorizedException,
|
||||||
|
NotFoundException,
|
||||||
|
ForbiddenException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
import { RegisterDto } from './dto/register.dto';
|
||||||
|
import { LoginDto } from './dto/login.dto';
|
||||||
|
import { EmailDto } from './dto/email.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private userRepo: Repository<User>,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async register(dto: RegisterDto): Promise<void> {
|
||||||
|
if (process.env.REGISTRATION_DISABLED === 'true') {
|
||||||
|
throw new ForbiddenException('Registration is disabled');
|
||||||
|
}
|
||||||
|
const existing = await this.userRepo.findOne({
|
||||||
|
where: { email: dto.email },
|
||||||
|
});
|
||||||
|
if (existing) throw new ConflictException('User already exists');
|
||||||
|
|
||||||
|
const hashed = await bcrypt.hash(dto.password, 10);
|
||||||
|
await this.userRepo.save({
|
||||||
|
email: dto.email,
|
||||||
|
password: hashed,
|
||||||
|
name: dto.name || '',
|
||||||
|
surname: dto.surname || '',
|
||||||
|
TermsAndConditions: dto.TermsAndConditions ?? true,
|
||||||
|
PrivacyPolicy: dto.PrivacyPolicy ?? true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(dto: LoginDto) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { email: dto.email } });
|
||||||
|
if (!user) throw new UnauthorizedException('Invalid credentials');
|
||||||
|
|
||||||
|
const valid = await bcrypt.compare(dto.password, user.password);
|
||||||
|
if (!valid) throw new UnauthorizedException('Invalid credentials');
|
||||||
|
|
||||||
|
return this.buildUserObject(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkExists(dto: EmailDto) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { email: dto.email } });
|
||||||
|
return { message: user ? 'User exists' : 'User not found' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async forgotPassword(dto: EmailDto): Promise<void> {
|
||||||
|
const user = await this.userRepo.findOne({ where: { email: dto.email } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildUserObject(user: User) {
|
||||||
|
const payload = { sub: user.id, email: user.email };
|
||||||
|
return {
|
||||||
|
access_token: this.jwtService.sign(payload),
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
surname: user.surname,
|
||||||
|
facebook_id: user.facebook_id,
|
||||||
|
TermsAndConditions: user.TermsAndConditions,
|
||||||
|
PrivacyPolicy: user.PrivacyPolicy,
|
||||||
|
SafeZoneSettings: {
|
||||||
|
Enabled: user.SafeZoneEnabled,
|
||||||
|
LastUpdate: new Date(user.Created).toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsEmail } from 'class-validator';
|
||||||
|
|
||||||
|
export class EmailDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
export class LoginDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
IsEmail,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsBoolean,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class RegisterDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
surname?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
TermsAndConditions?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
PrivacyPolicy?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Platform?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Strategy } from 'passport-jwt';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
import { Request } from 'express';
|
||||||
|
|
||||||
|
const extractJwt = (req: Request): string | null => {
|
||||||
|
if (!req.headers || !req.headers.authorization) return null;
|
||||||
|
const parts = req.headers.authorization.split(' ');
|
||||||
|
if (parts.length === 2 && parts[0].toLowerCase() === 'bearer')
|
||||||
|
return parts[1];
|
||||||
|
if (parts.length === 1) return parts[0];
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private userRepo: Repository<User>,
|
||||||
|
) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: extractJwt,
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: process.env.JWT_SECRET || 'fixed-smart-secret-key',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: { sub: number; email: string }) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { id: payload.sub } });
|
||||||
|
if (!user) throw new UnauthorizedException();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const CurrentUser = createParamDecorator(
|
||||||
|
(data: unknown, ctx: ExecutionContext) => {
|
||||||
|
const request = ctx.switchToHttp().getRequest();
|
||||||
|
return request.user;
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const IS_PUBLIC_KEY = 'isPublic';
|
||||||
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export abstract class BaseEntity {
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Created: string;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'datetime' })
|
||||||
|
Updated: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
|
||||||
|
let status = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
let message = 'Internal server error';
|
||||||
|
|
||||||
|
if (exception instanceof HttpException) {
|
||||||
|
status = exception.getStatus();
|
||||||
|
const res = exception.getResponse();
|
||||||
|
message =
|
||||||
|
typeof res === 'string'
|
||||||
|
? res
|
||||||
|
: (res as any).message || exception.message;
|
||||||
|
if (Array.isArray(message)) message = message[0];
|
||||||
|
} else if (exception instanceof Error) {
|
||||||
|
message = exception.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
response.status(status).json({ message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||||
|
constructor(private reflector: Reflector) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext) {
|
||||||
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (isPublic) return true;
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
Logger,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable, tap } from 'rxjs';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RequestLoggerInterceptor implements NestInterceptor {
|
||||||
|
private readonly logger = new Logger('HTTP');
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const ctx = context.switchToHttp();
|
||||||
|
const request = ctx.getRequest<Request>();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
|
||||||
|
const { method, originalUrl, query, headers, body } = request;
|
||||||
|
const id = crypto.randomBytes(4).toString('hex');
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
const sanitizedHeaders = { ...headers };
|
||||||
|
delete sanitizedHeaders.authorization;
|
||||||
|
delete sanitizedHeaders['authorization'];
|
||||||
|
delete sanitizedHeaders.cookie;
|
||||||
|
delete sanitizedHeaders['cookie'];
|
||||||
|
|
||||||
|
const sanitizedBody = this.sanitize(body);
|
||||||
|
|
||||||
|
this.logger.log(`[${id}] --> ${method} ${originalUrl}`);
|
||||||
|
this.logger.debug(`[${id}] Headers: ${JSON.stringify(sanitizedHeaders)}`);
|
||||||
|
if (Object.keys(query || {}).length > 0) {
|
||||||
|
this.logger.debug(`[${id}] Query: ${JSON.stringify(query)}`);
|
||||||
|
}
|
||||||
|
if (body && Object.keys(body).length > 0) {
|
||||||
|
this.logger.debug(`[${id}] Body: ${JSON.stringify(sanitizedBody)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap({
|
||||||
|
next: (resBody: any) => {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
const status = response.statusCode;
|
||||||
|
const level = status >= 400 ? 'warn' : 'log';
|
||||||
|
const sanitizedRes = this.sanitize(resBody);
|
||||||
|
const resStr = JSON.stringify(sanitizedRes);
|
||||||
|
const truncated =
|
||||||
|
resStr.length > 1000 ? resStr.slice(0, 1000) + '...' : resStr;
|
||||||
|
this.logger[level](
|
||||||
|
`[${id}] <-- ${method} ${originalUrl} ${status} ${duration}ms`,
|
||||||
|
);
|
||||||
|
this.logger.debug(`[${id}] Response: ${truncated}`);
|
||||||
|
},
|
||||||
|
error: (err: any) => {
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
this.logger.error(
|
||||||
|
`[${id}] <-- ${method} ${originalUrl} ${err.status || 500} ${duration}ms - ${err.message}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sanitize(obj: any): any {
|
||||||
|
if (!obj || typeof obj !== 'object') return obj;
|
||||||
|
const sensitive = [
|
||||||
|
'password',
|
||||||
|
'token',
|
||||||
|
'access_token',
|
||||||
|
'secret',
|
||||||
|
'authorization',
|
||||||
|
];
|
||||||
|
const clone = Array.isArray(obj) ? [...obj] : { ...obj };
|
||||||
|
for (const key of Object.keys(clone)) {
|
||||||
|
if (sensitive.some((s) => key.toLowerCase().includes(s.toLowerCase()))) {
|
||||||
|
clone[key] = '***';
|
||||||
|
} else if (typeof clone[key] === 'object' && clone[key] !== null) {
|
||||||
|
clone[key] = this.sanitize(clone[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { DevicesService } from './devices.service';
|
||||||
|
import { DeviceDto } from './dto/device.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('devices')
|
||||||
|
export class DevicesController {
|
||||||
|
constructor(private devicesService: DevicesService) {}
|
||||||
|
|
||||||
|
@Get('device')
|
||||||
|
findAll(@CurrentUser() user: User) {
|
||||||
|
return this.devicesService.findAll(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('device')
|
||||||
|
create(@CurrentUser() user: User, @Body() dto: DeviceDto) {
|
||||||
|
return this.devicesService.create(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('device')
|
||||||
|
update(@CurrentUser() user: User, @Body() dto: DeviceDto) {
|
||||||
|
return this.devicesService.update(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('device/batch')
|
||||||
|
batchUpdate(@CurrentUser() user: User, @Body() dtos: DeviceDto[]) {
|
||||||
|
return this.devicesService.batchUpdate(user.id, dtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('device/:DeviceID')
|
||||||
|
findOne(@CurrentUser() user: User, @Param('DeviceID') deviceId: string) {
|
||||||
|
return this.devicesService.findOne(user.id, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('device/:DeviceID')
|
||||||
|
delete(@CurrentUser() user: User, @Param('DeviceID') deviceId: string) {
|
||||||
|
return this.devicesService.delete(user.id, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('device/status/:DeviceID')
|
||||||
|
getStatus(@CurrentUser() user: User, @Param('DeviceID') deviceId: string) {
|
||||||
|
return this.devicesService.getStatus(user.id, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('category')
|
||||||
|
getCategories() {
|
||||||
|
return this.devicesService.getCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { DevicesController } from './devices.controller';
|
||||||
|
import { DevicesService } from './devices.service';
|
||||||
|
import { Device } from './entities/device.entity';
|
||||||
|
import { DeviceCategory } from './entities/device-category.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Device, DeviceCategory])],
|
||||||
|
controllers: [DevicesController],
|
||||||
|
providers: [DevicesService],
|
||||||
|
exports: [DevicesService],
|
||||||
|
})
|
||||||
|
export class DevicesModule {}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Device } from './entities/device.entity';
|
||||||
|
import { DeviceCategory } from './entities/device-category.entity';
|
||||||
|
import { DeviceDto } from './dto/device.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DevicesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Device)
|
||||||
|
private deviceRepo: Repository<Device>,
|
||||||
|
@InjectRepository(DeviceCategory)
|
||||||
|
private categoryRepo: Repository<DeviceCategory>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(userId: number) {
|
||||||
|
const devices = await this.deviceRepo.find({
|
||||||
|
where: { userId },
|
||||||
|
relations: { Category: true },
|
||||||
|
order: { Order: 'ASC' },
|
||||||
|
});
|
||||||
|
return devices.map((d) => ({
|
||||||
|
DeviceID: d.DeviceID,
|
||||||
|
Name: d.Name,
|
||||||
|
DeviceType: d.DeviceType,
|
||||||
|
CategoryID: d.CategoryID,
|
||||||
|
Authorization: d.Authorization,
|
||||||
|
Order: d.Order,
|
||||||
|
Category: d.Category || null,
|
||||||
|
Share: d.Share,
|
||||||
|
Created: d.Created,
|
||||||
|
FirmwareRevision: d.FirmwareRevision,
|
||||||
|
HardwareRevision: d.HardwareRevision,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(userId: number, dto: DeviceDto) {
|
||||||
|
const device = this.deviceRepo.create({
|
||||||
|
...dto,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
const saved = await this.deviceRepo.save(device);
|
||||||
|
return this.findOne(userId, saved.DeviceID);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(userId: number, dto: DeviceDto) {
|
||||||
|
const device = await this.deviceRepo.findOne({
|
||||||
|
where: { DeviceID: dto.DeviceID, userId },
|
||||||
|
});
|
||||||
|
if (!device) throw new NotFoundException('Device not found');
|
||||||
|
|
||||||
|
if (dto.Name !== undefined) device.Name = dto.Name;
|
||||||
|
if (dto.DeviceType !== undefined) device.DeviceType = dto.DeviceType;
|
||||||
|
if (dto.CategoryID !== undefined) device.CategoryID = dto.CategoryID;
|
||||||
|
if (dto.Order !== undefined) device.Order = dto.Order;
|
||||||
|
|
||||||
|
await this.deviceRepo.save(device);
|
||||||
|
return this.findOne(userId, device.DeviceID);
|
||||||
|
}
|
||||||
|
|
||||||
|
async batchUpdate(userId: number, dtos: DeviceDto[]) {
|
||||||
|
const results: DeviceDto[] = [];
|
||||||
|
for (const dto of dtos) {
|
||||||
|
const updated = await this.update(userId, dto);
|
||||||
|
results.push(updated);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(userId: number, deviceId: string) {
|
||||||
|
const device = await this.deviceRepo.findOne({
|
||||||
|
where: { DeviceID: deviceId, userId },
|
||||||
|
relations: { Category: true },
|
||||||
|
});
|
||||||
|
if (!device) throw new NotFoundException('Device not found');
|
||||||
|
return {
|
||||||
|
DeviceID: device.DeviceID,
|
||||||
|
Name: device.Name,
|
||||||
|
DeviceType: device.DeviceType,
|
||||||
|
CategoryID: device.CategoryID,
|
||||||
|
Authorization: device.Authorization,
|
||||||
|
Order: device.Order,
|
||||||
|
Category: device.Category || null,
|
||||||
|
Share: device.Share,
|
||||||
|
Created: device.Created,
|
||||||
|
FirmwareRevision: device.FirmwareRevision,
|
||||||
|
HardwareRevision: device.HardwareRevision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(userId: number, deviceId: string): Promise<void> {
|
||||||
|
const result = await this.deviceRepo.delete({
|
||||||
|
DeviceID: deviceId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
if (result.affected === 0) throw new NotFoundException('Device not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStatus(userId: number, deviceId: string) {
|
||||||
|
await this.findOne(userId, deviceId);
|
||||||
|
return [
|
||||||
|
{ Name: 'Connected', Value: 'true' },
|
||||||
|
{ Name: 'Battery', Value: '85' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCategories() {
|
||||||
|
return this.categoryRepo.find();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class DeviceDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DeviceType: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
CategoryID?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Authorization?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Order?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
Share?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('device_categories')
|
||||||
|
export class DeviceCategory {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
Id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
Image: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryColumn,
|
||||||
|
Column,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
CreateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { DeviceCategory } from './device-category.entity';
|
||||||
|
|
||||||
|
@Entity('devices')
|
||||||
|
export class Device {
|
||||||
|
@PrimaryColumn()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
DeviceType: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
CategoryID: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
Authorization: string;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
Order: number;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
Share: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
FirmwareRevision: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
HardwareRevision: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Created: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => DeviceCategory)
|
||||||
|
@JoinColumn({ name: 'CategoryID' })
|
||||||
|
Category: DeviceCategory;
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import {
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
IsArray,
|
||||||
|
ValidateNested,
|
||||||
|
IsObject,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
class LocationDto {
|
||||||
|
@IsNumber()
|
||||||
|
lat: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
lng: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Altitude?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Accuracy?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventLogDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
EventType: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Battery?: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Time: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Value?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => LocationDto)
|
||||||
|
Location?: LocationDto;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
OriginDeviceID?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
OriginDeviceName?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
OriginDeviceType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HistoryFilterDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
EventType: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
DateRange?: { From?: string; To?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValueHistoryDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
EventType: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
DateRange?: { From?: string; To?: string };
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
TimeSlice?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EventFilterDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Count?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
Device?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
EventType?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
DateRange?: { From?: string; To?: string };
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('event_logs')
|
||||||
|
@Index(['DeviceID', 'EventType'])
|
||||||
|
export class EventLog {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
EventType: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
Battery: number;
|
||||||
|
|
||||||
|
@Column({ type: 'datetime' })
|
||||||
|
Time: string;
|
||||||
|
|
||||||
|
@Column({ type: 'real', nullable: true })
|
||||||
|
Value: number;
|
||||||
|
|
||||||
|
@Column({ type: 'real', nullable: true })
|
||||||
|
lat: number;
|
||||||
|
|
||||||
|
@Column({ type: 'real', nullable: true })
|
||||||
|
lng: number;
|
||||||
|
|
||||||
|
@Column({ type: 'real', nullable: true })
|
||||||
|
Altitude: number;
|
||||||
|
|
||||||
|
@Column({ type: 'real', nullable: true })
|
||||||
|
Accuracy: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
OriginDeviceID: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
OriginDeviceName: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
OriginDeviceType: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
userId: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
import { EventsService } from './events.service';
|
||||||
|
import { EventLogDto, HistoryFilterDto, ValueHistoryDto, EventFilterDto } from './dto/event-log.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('devices/event')
|
||||||
|
export class EventsController {
|
||||||
|
constructor(private eventsService: EventsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
save(@CurrentUser() user: User, @Body() dto: EventLogDto) {
|
||||||
|
return this.eventsService.saveEvent(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk')
|
||||||
|
saveBulk(@CurrentUser() user: User, @Body() dtos: EventLogDto[]) {
|
||||||
|
return this.eventsService.saveBulk(user.id, dtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('bulk-data')
|
||||||
|
saveBulkData(@CurrentUser() user: User, @Body() dtos: EventLogDto[]) {
|
||||||
|
return this.eventsService.saveBulk(user.id, dtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('history')
|
||||||
|
getHistory(@CurrentUser() user: User, @Body() dto: HistoryFilterDto) {
|
||||||
|
return this.eventsService.getHistory(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('value/history')
|
||||||
|
getValueHistory(@CurrentUser() user: User, @Body() dto: ValueHistoryDto) {
|
||||||
|
return this.eventsService.getValueHistory(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('filter')
|
||||||
|
getFiltered(@CurrentUser() user: User, @Body() dto: EventFilterDto) {
|
||||||
|
return this.eventsService.getFilteredEvents(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('filters')
|
||||||
|
getFilteredFlat(@CurrentUser() user: User, @Body() dto: EventFilterDto) {
|
||||||
|
return this.eventsService.getFilteredEventsFlat(user.id, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { EventsController } from './events.controller';
|
||||||
|
import { EventsService } from './events.service';
|
||||||
|
import { EventLog } from './entities/event-log.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([EventLog])],
|
||||||
|
controllers: [EventsController],
|
||||||
|
providers: [EventsService],
|
||||||
|
})
|
||||||
|
export class EventsModule {}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository, Between, In } from 'typeorm';
|
||||||
|
import { EventLog } from './entities/event-log.entity';
|
||||||
|
import { EventLogDto, HistoryFilterDto, ValueHistoryDto, EventFilterDto } from './dto/event-log.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EventsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(EventLog)
|
||||||
|
private eventRepo: Repository<EventLog>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async saveEvent(userId: number, dto: EventLogDto) {
|
||||||
|
const event = this.eventRepo.create({
|
||||||
|
DeviceID: dto.DeviceID,
|
||||||
|
EventType: dto.EventType,
|
||||||
|
Battery: dto.Battery,
|
||||||
|
Time: dto.Time,
|
||||||
|
Value: dto.Value,
|
||||||
|
lat: dto.Location?.lat,
|
||||||
|
lng: dto.Location?.lng,
|
||||||
|
Altitude: dto.Location?.Altitude,
|
||||||
|
Accuracy: dto.Location?.Accuracy,
|
||||||
|
OriginDeviceID: dto.OriginDeviceID,
|
||||||
|
OriginDeviceName: dto.OriginDeviceName,
|
||||||
|
OriginDeviceType: dto.OriginDeviceType,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
const saved = await this.eventRepo.save(event);
|
||||||
|
return {
|
||||||
|
DeviceID: saved.DeviceID,
|
||||||
|
EventType: saved.EventType,
|
||||||
|
Battery: saved.Battery,
|
||||||
|
Time: saved.Time,
|
||||||
|
Value: saved.Value,
|
||||||
|
Location: saved.lat != null ? {
|
||||||
|
lat: saved.lat,
|
||||||
|
lng: saved.lng,
|
||||||
|
Altitude: saved.Altitude,
|
||||||
|
Accuracy: saved.Accuracy,
|
||||||
|
} : undefined,
|
||||||
|
OriginDeviceID: saved.OriginDeviceID,
|
||||||
|
OriginDeviceName: saved.OriginDeviceName,
|
||||||
|
OriginDeviceType: saved.OriginDeviceType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBulk(userId: number, dtos: EventLogDto[]): Promise<void> {
|
||||||
|
const events = dtos.map((dto) =>
|
||||||
|
this.eventRepo.create({
|
||||||
|
DeviceID: dto.DeviceID,
|
||||||
|
EventType: dto.EventType,
|
||||||
|
Battery: dto.Battery,
|
||||||
|
Time: dto.Time,
|
||||||
|
Value: dto.Value,
|
||||||
|
lat: dto.Location?.lat,
|
||||||
|
lng: dto.Location?.lng,
|
||||||
|
Altitude: dto.Location?.Altitude,
|
||||||
|
Accuracy: dto.Location?.Accuracy,
|
||||||
|
OriginDeviceID: dto.OriginDeviceID,
|
||||||
|
OriginDeviceName: dto.OriginDeviceName,
|
||||||
|
OriginDeviceType: dto.OriginDeviceType,
|
||||||
|
userId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await this.eventRepo.save(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getHistory(userId: number, dto: HistoryFilterDto) {
|
||||||
|
const where: any = { DeviceID: dto.DeviceID, EventType: dto.EventType, userId };
|
||||||
|
if (dto.DateRange?.From && dto.DateRange?.To) {
|
||||||
|
where.Time = Between(dto.DateRange.From, dto.DateRange.To);
|
||||||
|
}
|
||||||
|
const events = await this.eventRepo.find({ where, order: { Time: 'ASC' } });
|
||||||
|
return events.map((e) => ({ Time: e.Time, Value: e.Value }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getValueHistory(userId: number, dto: ValueHistoryDto) {
|
||||||
|
const where: any = { DeviceID: dto.DeviceID, EventType: dto.EventType, userId };
|
||||||
|
if (dto.DateRange?.From && dto.DateRange?.To) {
|
||||||
|
where.Time = Between(dto.DateRange.From, dto.DateRange.To);
|
||||||
|
}
|
||||||
|
const events = await this.eventRepo.find({ where, order: { Time: 'ASC' } });
|
||||||
|
const slice = dto.TimeSlice || 60;
|
||||||
|
const buckets: { [key: string]: { sum: number; count: number } } = {};
|
||||||
|
|
||||||
|
for (const e of events) {
|
||||||
|
const d = new Date(e.Time);
|
||||||
|
const bucket = new Date(Math.floor(d.getTime() / (slice * 60000)) * (slice * 60000)).toISOString();
|
||||||
|
if (!buckets[bucket]) buckets[bucket] = { sum: 0, count: 0 };
|
||||||
|
buckets[bucket].sum += e.Value || 0;
|
||||||
|
buckets[bucket].count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(buckets).map(([date, { sum, count }]) => ({
|
||||||
|
date,
|
||||||
|
avg: count > 0 ? sum / count : 0,
|
||||||
|
count,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFilteredEvents(userId: number, dto: EventFilterDto) {
|
||||||
|
const where: any = { userId };
|
||||||
|
if (dto.Device?.length) where.DeviceID = In(dto.Device);
|
||||||
|
if (dto.EventType?.length) where.EventType = In(dto.EventType);
|
||||||
|
if (dto.DateRange?.From && dto.DateRange?.To) {
|
||||||
|
where.Time = Between(dto.DateRange.From, dto.DateRange.To);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await this.eventRepo.find({
|
||||||
|
where,
|
||||||
|
order: { Time: 'DESC' },
|
||||||
|
take: dto.Count || 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped: any = {};
|
||||||
|
for (const e of events) {
|
||||||
|
if (!grouped[e.EventType]) grouped[e.EventType] = [];
|
||||||
|
grouped[e.EventType].push(this.mapEvent(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTypes = [
|
||||||
|
'LocationChange', 'Connected', 'Disconnected', 'BatteryChange',
|
||||||
|
'MotionDetected', 'KeyPressed', 'TemperatureChanged', 'HumidityChanged',
|
||||||
|
];
|
||||||
|
for (const t of defaultTypes) {
|
||||||
|
if (!grouped[t]) grouped[t] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return grouped;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFilteredEventsFlat(userId: number, dto: EventFilterDto) {
|
||||||
|
const where: any = { userId };
|
||||||
|
if (dto.Device?.length) where.DeviceID = In(dto.Device);
|
||||||
|
if (dto.EventType?.length) where.EventType = In(dto.EventType);
|
||||||
|
if (dto.DateRange?.From && dto.DateRange?.To) {
|
||||||
|
where.Time = Between(dto.DateRange.From, dto.DateRange.To);
|
||||||
|
}
|
||||||
|
|
||||||
|
const events = await this.eventRepo.find({
|
||||||
|
where,
|
||||||
|
order: { Time: 'DESC' },
|
||||||
|
take: dto.Count || 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
return events.map((e) => this.mapEvent(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapEvent(e: EventLog) {
|
||||||
|
return {
|
||||||
|
DeviceID: e.DeviceID,
|
||||||
|
EventType: e.EventType,
|
||||||
|
Battery: e.Battery,
|
||||||
|
Time: e.Time,
|
||||||
|
Value: e.Value,
|
||||||
|
Location: e.lat != null
|
||||||
|
? { lat: e.lat, lng: e.lng, Altitude: e.Altitude, Accuracy: e.Accuracy }
|
||||||
|
: undefined,
|
||||||
|
OriginDeviceID: e.OriginDeviceID,
|
||||||
|
OriginDeviceName: e.OriginDeviceName,
|
||||||
|
OriginDeviceType: e.OriginDeviceType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Controller, Get, Param } from '@nestjs/common';
|
||||||
|
import { FaqService } from './faq.service';
|
||||||
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class FaqController {
|
||||||
|
constructor(private faqService: FaqService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('faq/:type')
|
||||||
|
getByType(@Param('type') type: string) {
|
||||||
|
return this.faqService.getByType(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('new-faq')
|
||||||
|
getAll() {
|
||||||
|
return this.faqService.getAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { FaqController } from './faq.controller';
|
||||||
|
import { FaqService } from './faq.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [FaqController],
|
||||||
|
providers: [FaqService],
|
||||||
|
})
|
||||||
|
export class FaqModule {}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FaqService {
|
||||||
|
async getByType(type: string) {
|
||||||
|
return [
|
||||||
|
{ title: `What is ${type}?`, text: `Answer about ${type}.`, order: 1 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
category: 'General',
|
||||||
|
faq: [
|
||||||
|
{ title: 'What is Fixed?', text: 'Fixed is a tracking device.', order: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IsNumber } from 'class-validator';
|
||||||
|
|
||||||
|
export class GeolocateDto {
|
||||||
|
@IsNumber()
|
||||||
|
Latitude: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
Longitude: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
import { GeolocationService } from './geolocation.service';
|
||||||
|
import { GeolocateDto } from './dto/geolocation.dto';
|
||||||
|
|
||||||
|
@Controller('geolocate')
|
||||||
|
export class GeolocationController {
|
||||||
|
constructor(private geolocationService: GeolocationService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
reverseGeocode(@Body() dto: GeolocateDto) {
|
||||||
|
return this.geolocationService.reverseGeocode(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GeolocationController } from './geolocation.controller';
|
||||||
|
import { GeolocationService } from './geolocation.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [GeolocationController],
|
||||||
|
providers: [GeolocationService],
|
||||||
|
})
|
||||||
|
export class GeolocationModule {}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { GeolocateDto } from './dto/geolocation.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GeolocationService {
|
||||||
|
async reverseGeocode(dto: GeolocateDto) {
|
||||||
|
return {
|
||||||
|
formatted_address: `${dto.Latitude}, ${dto.Longitude}`,
|
||||||
|
street_number: '',
|
||||||
|
route: '',
|
||||||
|
locality: '',
|
||||||
|
political: '',
|
||||||
|
postal_code: '',
|
||||||
|
country: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber } from 'class-validator';
|
||||||
|
|
||||||
|
export class NewLimitDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
EventType: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Type?: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
Value: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Active?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateLimitDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Type?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Value?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Active?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FilterLimitDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
DeviceID?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
EventType?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Type?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Active?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('device_limits')
|
||||||
|
export class DeviceLimit {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
Id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
EventType: string;
|
||||||
|
|
||||||
|
@Column({ default: 'UP' })
|
||||||
|
Type: string;
|
||||||
|
|
||||||
|
@Column({ type: 'real' })
|
||||||
|
Value: number;
|
||||||
|
|
||||||
|
@Column({ default: 1 })
|
||||||
|
Active: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Created: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Updated: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Controller, Get, Post, Put, Delete, Body, Param } from '@nestjs/common';
|
||||||
|
import { LimitsService } from './limits.service';
|
||||||
|
import { NewLimitDto, UpdateLimitDto, FilterLimitDto } from './dto/limit.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('device/limit')
|
||||||
|
export class LimitsController {
|
||||||
|
constructor(private limitsService: LimitsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@CurrentUser() user: User, @Body() dto: NewLimitDto) {
|
||||||
|
return this.limitsService.create(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(@CurrentUser() user: User) {
|
||||||
|
return this.limitsService.findAll(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@CurrentUser() user: User, @Param('id') id: number) {
|
||||||
|
return this.limitsService.findOne(user.id, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
update(@CurrentUser() user: User, @Param('id') id: number, @Body() dto: UpdateLimitDto) {
|
||||||
|
return this.limitsService.update(user.id, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('filter')
|
||||||
|
filter(@CurrentUser() user: User, @Body() dto: FilterLimitDto) {
|
||||||
|
return this.limitsService.filter(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
delete(@CurrentUser() user: User, @Param('id') id: number) {
|
||||||
|
return this.limitsService.delete(user.id, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { LimitsController } from './limits.controller';
|
||||||
|
import { LimitsService } from './limits.service';
|
||||||
|
import { DeviceLimit } from './entities/device-limit.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([DeviceLimit])],
|
||||||
|
controllers: [LimitsController],
|
||||||
|
providers: [LimitsService],
|
||||||
|
})
|
||||||
|
export class LimitsModule {}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { DeviceLimit } from './entities/device-limit.entity';
|
||||||
|
import { NewLimitDto, UpdateLimitDto, FilterLimitDto } from './dto/limit.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LimitsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(DeviceLimit)
|
||||||
|
private limitRepo: Repository<DeviceLimit>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private mapLimit(l: DeviceLimit) {
|
||||||
|
return {
|
||||||
|
Id: l.Id,
|
||||||
|
DeviceID: l.DeviceID,
|
||||||
|
EventType: l.EventType,
|
||||||
|
Type: l.Type,
|
||||||
|
Value: l.Value,
|
||||||
|
Active: l.Active,
|
||||||
|
Created: l.Created,
|
||||||
|
Updated: l.Updated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(userId: number, dto: NewLimitDto) {
|
||||||
|
const limit = this.limitRepo.create({
|
||||||
|
...dto,
|
||||||
|
userId,
|
||||||
|
Type: dto.Type || 'UP',
|
||||||
|
Active: dto.Active ?? 1,
|
||||||
|
});
|
||||||
|
const saved = await this.limitRepo.save(limit);
|
||||||
|
return this.mapLimit(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(userId: number) {
|
||||||
|
const limits = await this.limitRepo.find({ where: { userId } });
|
||||||
|
return limits.map((l) => this.mapLimit(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(userId: number, id: number) {
|
||||||
|
const limit = await this.limitRepo.findOne({ where: { Id: id, userId } });
|
||||||
|
if (!limit) throw new NotFoundException('Limit not found');
|
||||||
|
return this.mapLimit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(userId: number, id: number, dto: UpdateLimitDto) {
|
||||||
|
const limit = await this.limitRepo.findOne({ where: { Id: id, userId } });
|
||||||
|
if (!limit) throw new NotFoundException('Limit not found');
|
||||||
|
if (dto.Type !== undefined) limit.Type = dto.Type;
|
||||||
|
if (dto.Value !== undefined) limit.Value = dto.Value;
|
||||||
|
if (dto.Active !== undefined) limit.Active = dto.Active;
|
||||||
|
await this.limitRepo.save(limit);
|
||||||
|
return this.mapLimit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
async filter(userId: number, dto: FilterLimitDto) {
|
||||||
|
const where: any = { userId };
|
||||||
|
if (dto.DeviceID) where.DeviceID = dto.DeviceID;
|
||||||
|
if (dto.EventType) where.EventType = dto.EventType;
|
||||||
|
if (dto.Type) where.Type = dto.Type;
|
||||||
|
if (dto.Active !== undefined) where.Active = dto.Active;
|
||||||
|
const limits = await this.limitRepo.find({ where });
|
||||||
|
return limits.map((l) => this.mapLimit(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(userId: number, id: number): Promise<void> {
|
||||||
|
const result = await this.limitRepo.delete({ Id: id, userId });
|
||||||
|
if (result.affected === 0) throw new NotFoundException('Limit not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import { RequestLoggerInterceptor } from './common/interceptors/logger.interceptor';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule, {
|
||||||
|
logger: ['log', 'error', 'warn', 'debug', 'verbose'],
|
||||||
|
});
|
||||||
|
|
||||||
|
app.useGlobalPipes(
|
||||||
|
new ValidationPipe({
|
||||||
|
whitelist: true,
|
||||||
|
transform: true,
|
||||||
|
forbidNonWhitelisted: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
app.useGlobalInterceptors(new RequestLoggerInterceptor());
|
||||||
|
|
||||||
|
app.setGlobalPrefix('api/v1');
|
||||||
|
app.enableCors();
|
||||||
|
|
||||||
|
const ds = app.get(DataSource);
|
||||||
|
await ds.query(
|
||||||
|
`INSERT OR IGNORE INTO device_categories (Id, Name, Image) VALUES
|
||||||
|
(1, 'Trackers', ''),
|
||||||
|
(2, 'Pets', ''),
|
||||||
|
(3, 'Keys', ''),
|
||||||
|
(4, 'Vehicles', ''),
|
||||||
|
(5, 'Luggage', '')`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await app.listen(3000, '0.0.0.0');
|
||||||
|
console.log('API running on http://0.0.0.0:3000');
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap().catch(console.error);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { MiscService } from './misc.service';
|
||||||
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class MiscController {
|
||||||
|
constructor(private miscService: MiscService) {}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('languages')
|
||||||
|
getLanguages() {
|
||||||
|
return this.miscService.getLanguages();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('shops')
|
||||||
|
getShops() {
|
||||||
|
return this.miscService.getShops();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { MiscController } from './misc.controller';
|
||||||
|
import { MiscService } from './misc.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [MiscController],
|
||||||
|
providers: [MiscService],
|
||||||
|
})
|
||||||
|
export class MiscModule {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MiscService {
|
||||||
|
async getLanguages() {
|
||||||
|
return { en: 'English', cs: 'Čeština' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getShops() {
|
||||||
|
return [
|
||||||
|
{ Name: 'Fixed Shop', Image: '', Link: 'https://example.com/shop' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { IsBoolean, IsNumber } from 'class-validator';
|
||||||
|
|
||||||
|
export class NightModeDto {
|
||||||
|
@IsBoolean()
|
||||||
|
Allow: boolean;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
From: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
To: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('night_mode')
|
||||||
|
export class NightMode {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
Allow: boolean;
|
||||||
|
|
||||||
|
@Column({ default: 22 })
|
||||||
|
From: number;
|
||||||
|
|
||||||
|
@Column({ default: 7 })
|
||||||
|
To: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Controller, Get, Put, Body } from '@nestjs/common';
|
||||||
|
import { NightModeService } from './night-mode.service';
|
||||||
|
import { NightModeDto } from './dto/night-mode.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('user/night-mode')
|
||||||
|
export class NightModeController {
|
||||||
|
constructor(private nightModeService: NightModeService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
get(@CurrentUser() user: User) {
|
||||||
|
return this.nightModeService.get(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
update(@CurrentUser() user: User, @Body() dto: NightModeDto) {
|
||||||
|
return this.nightModeService.update(user.id, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { NightModeController } from './night-mode.controller';
|
||||||
|
import { NightModeService } from './night-mode.service';
|
||||||
|
import { NightMode } from './entities/night-mode.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([NightMode])],
|
||||||
|
controllers: [NightModeController],
|
||||||
|
providers: [NightModeService],
|
||||||
|
})
|
||||||
|
export class NightModeModule {}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { NightMode } from './entities/night-mode.entity';
|
||||||
|
import { NightModeDto } from './dto/night-mode.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class NightModeService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(NightMode)
|
||||||
|
private nightModeRepo: Repository<NightMode>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async get(userId: number) {
|
||||||
|
let nm = await this.nightModeRepo.findOne({ where: { userId } });
|
||||||
|
if (!nm) {
|
||||||
|
nm = this.nightModeRepo.create({ userId });
|
||||||
|
await this.nightModeRepo.save(nm);
|
||||||
|
}
|
||||||
|
return { Allow: nm.Allow, From: nm.From, To: nm.To };
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(userId: number, dto: NightModeDto) {
|
||||||
|
let nm = await this.nightModeRepo.findOne({ where: { userId } });
|
||||||
|
if (!nm) {
|
||||||
|
nm = this.nightModeRepo.create({ userId });
|
||||||
|
}
|
||||||
|
nm.Allow = dto.Allow;
|
||||||
|
nm.From = dto.From;
|
||||||
|
nm.To = dto.To;
|
||||||
|
await this.nightModeRepo.save(nm);
|
||||||
|
return { Allow: nm.Allow, From: nm.From, To: nm.To };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { IsString, IsOptional } from 'class-validator';
|
||||||
|
|
||||||
|
export class PushRegisterDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Token: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Platform: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushRequestBeepDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
SenderDeviceID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushRequestRefreshDto {
|
||||||
|
@IsString()
|
||||||
|
SenderDeviceID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PushRemoteDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Action: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Controller, Post, Get, Body, Query } from '@nestjs/common';
|
||||||
|
import { PushService } from './push.service';
|
||||||
|
import { PushRegisterDto, PushRequestBeepDto, PushRequestRefreshDto, PushRemoteDto } from './dto/push.dto';
|
||||||
|
import { Public } from '../common/decorators/public.decorator';
|
||||||
|
|
||||||
|
@Controller('push')
|
||||||
|
export class PushController {
|
||||||
|
constructor(private pushService: PushService) {}
|
||||||
|
|
||||||
|
@Post('register')
|
||||||
|
register(@Body() dto: PushRegisterDto) {
|
||||||
|
return this.pushService.register(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('request-beep')
|
||||||
|
requestBeep(@Body() dto: PushRequestBeepDto) {
|
||||||
|
return this.pushService.requestBeep(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('request-refresh')
|
||||||
|
requestRefresh(@Body() dto: PushRequestRefreshDto) {
|
||||||
|
return this.pushService.requestRefresh(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('remote-push')
|
||||||
|
remotePush(@Body() dto: PushRemoteDto) {
|
||||||
|
return this.pushService.remotePush(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('test')
|
||||||
|
test(@Query('random') random: string) {
|
||||||
|
return this.pushService.test();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PushController } from './push.controller';
|
||||||
|
import { PushService } from './push.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [PushController],
|
||||||
|
providers: [PushService],
|
||||||
|
})
|
||||||
|
export class PushModule {}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PushRegisterDto, PushRequestBeepDto, PushRequestRefreshDto, PushRemoteDto } from './dto/push.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PushService {
|
||||||
|
private tokens: any[] = [];
|
||||||
|
|
||||||
|
async register(dto: PushRegisterDto) {
|
||||||
|
this.tokens.push(dto);
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestBeep(dto: PushRequestBeepDto) {
|
||||||
|
return { Count: 1, SuccessCount: 1, SuccessFailed: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async requestRefresh(dto: PushRequestRefreshDto) {
|
||||||
|
return { Count: 1, SuccessCount: 1, SuccessFailed: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async remotePush(dto: PushRemoteDto) {
|
||||||
|
return { Count: 1, SuccessCount: 1, SuccessFailed: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
async test() {
|
||||||
|
return { Count: 1, SuccessCount: 1, SuccessFailed: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { IsString, IsOptional, IsObject } from 'class-validator';
|
||||||
|
|
||||||
|
export class RouteBodyDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
DateRange?: { From?: string; To?: string };
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
import { RouteService } from './route.service';
|
||||||
|
import { RouteBodyDto } from './dto/route.dto';
|
||||||
|
|
||||||
|
@Controller('route')
|
||||||
|
export class RouteController {
|
||||||
|
constructor(private routeService: RouteService) {}
|
||||||
|
|
||||||
|
@Post('list')
|
||||||
|
getRoutes(@Body() dto: RouteBodyDto) {
|
||||||
|
return this.routeService.getRoutes(dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { RouteController } from './route.controller';
|
||||||
|
import { RouteService } from './route.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [RouteController],
|
||||||
|
providers: [RouteService],
|
||||||
|
})
|
||||||
|
export class RouteModule {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { RouteBodyDto } from './dto/route.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RouteService {
|
||||||
|
async getRoutes(dto: RouteBodyDto) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, IsBoolean, IsObject } from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
|
||||||
|
class SafeZoneLocationDto {
|
||||||
|
@IsNumber()
|
||||||
|
Lat: number;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
Lng: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SafeZoneDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
@Type(() => SafeZoneLocationDto)
|
||||||
|
Location?: SafeZoneLocationDto;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Radius?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
WifiName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SafeZoneSettingsDto {
|
||||||
|
@IsBoolean()
|
||||||
|
Enabled: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('safe_zones')
|
||||||
|
export class SafeZone {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
Id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'real' })
|
||||||
|
Lat: number;
|
||||||
|
|
||||||
|
@Column({ type: 'real' })
|
||||||
|
Lng: number;
|
||||||
|
|
||||||
|
@Column({ default: 100 })
|
||||||
|
Radius: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
WifiName: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Created: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SafeZonesService } from './safe-zones.service';
|
||||||
|
import { SafeZoneDto, SafeZoneSettingsDto } from './dto/safe-zone.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('user/safe-zone')
|
||||||
|
export class SafeZonesController {
|
||||||
|
constructor(private safeZonesService: SafeZonesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(@CurrentUser() user: User) {
|
||||||
|
return this.safeZonesService.findAll(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@CurrentUser() user: User, @Body() dto: SafeZoneDto) {
|
||||||
|
return this.safeZonesService.create(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
update(
|
||||||
|
@CurrentUser() user: User,
|
||||||
|
@Query('id') id: number,
|
||||||
|
@Body() dto: SafeZoneDto,
|
||||||
|
) {
|
||||||
|
return this.safeZonesService.update(user.id, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete()
|
||||||
|
delete(@CurrentUser() user: User, @Query('id') id: number) {
|
||||||
|
return this.safeZonesService.delete(user.id, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('settings')
|
||||||
|
getSettings(@CurrentUser() user: User) {
|
||||||
|
return this.safeZonesService.getSettings(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('settings')
|
||||||
|
updateSettings(@CurrentUser() user: User, @Body() dto: SafeZoneSettingsDto) {
|
||||||
|
return this.safeZonesService.updateSettings(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('default-radius')
|
||||||
|
getDefaultRadius() {
|
||||||
|
return this.safeZonesService.getDefaultRadius();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SafeZonesController } from './safe-zones.controller';
|
||||||
|
import { SafeZonesService } from './safe-zones.service';
|
||||||
|
import { SafeZone } from './entities/safe-zone.entity';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([SafeZone, User])],
|
||||||
|
controllers: [SafeZonesController],
|
||||||
|
providers: [SafeZonesService],
|
||||||
|
})
|
||||||
|
export class SafeZonesModule {}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { SafeZone } from './entities/safe-zone.entity';
|
||||||
|
import { SafeZoneDto, SafeZoneSettingsDto } from './dto/safe-zone.dto';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SafeZonesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(SafeZone)
|
||||||
|
private zoneRepo: Repository<SafeZone>,
|
||||||
|
@InjectRepository(User)
|
||||||
|
private userRepo: Repository<User>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(userId: number) {
|
||||||
|
const zones = await this.zoneRepo.find({
|
||||||
|
where: { userId },
|
||||||
|
order: { Created: 'DESC' },
|
||||||
|
});
|
||||||
|
return zones.map((z) => ({
|
||||||
|
Id: z.Id,
|
||||||
|
Name: z.Name,
|
||||||
|
Location: { Lat: z.Lat, Lng: z.Lng },
|
||||||
|
Radius: z.Radius,
|
||||||
|
WifiName: z.WifiName,
|
||||||
|
Created: z.Created,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(userId: number, dto: SafeZoneDto) {
|
||||||
|
const zone = this.zoneRepo.create({
|
||||||
|
Name: dto.Name || '',
|
||||||
|
Lat: dto.Location?.Lat || 0,
|
||||||
|
Lng: dto.Location?.Lng || 0,
|
||||||
|
Radius: dto.Radius || 100,
|
||||||
|
WifiName: dto.WifiName,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
const saved = await this.zoneRepo.save(zone);
|
||||||
|
return {
|
||||||
|
Id: saved.Id,
|
||||||
|
Name: saved.Name,
|
||||||
|
Location: { Lat: saved.Lat, Lng: saved.Lng },
|
||||||
|
Radius: saved.Radius,
|
||||||
|
WifiName: saved.WifiName,
|
||||||
|
Created: saved.Created,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(userId: number, id: number, dto: SafeZoneDto) {
|
||||||
|
const zone = await this.zoneRepo.findOne({ where: { Id: id, userId } });
|
||||||
|
if (!zone) throw new NotFoundException('Safe zone not found');
|
||||||
|
|
||||||
|
if (dto.Name !== undefined) zone.Name = dto.Name;
|
||||||
|
if (dto.Location) {
|
||||||
|
if (dto.Location.Lat !== undefined) zone.Lat = dto.Location.Lat;
|
||||||
|
if (dto.Location.Lng !== undefined) zone.Lng = dto.Location.Lng;
|
||||||
|
}
|
||||||
|
if (dto.Radius !== undefined) zone.Radius = dto.Radius;
|
||||||
|
if (dto.WifiName !== undefined) zone.WifiName = dto.WifiName;
|
||||||
|
|
||||||
|
await this.zoneRepo.save(zone);
|
||||||
|
return {
|
||||||
|
Id: zone.Id,
|
||||||
|
Name: zone.Name,
|
||||||
|
Location: { Lat: zone.Lat, Lng: zone.Lng },
|
||||||
|
Radius: zone.Radius,
|
||||||
|
WifiName: zone.WifiName,
|
||||||
|
Created: zone.Created,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(userId: number, id: number): Promise<void> {
|
||||||
|
const result = await this.zoneRepo.delete({ Id: id, userId });
|
||||||
|
if (result.affected === 0)
|
||||||
|
throw new NotFoundException('Safe zone not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSettings(userId: number) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { id: userId } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
return {
|
||||||
|
Enabled: user.SafeZoneEnabled,
|
||||||
|
LastUpdate: new Date(user.Created as any).toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(userId: number, dto: SafeZoneSettingsDto) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { id: userId } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
user.SafeZoneEnabled = dto.Enabled;
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
return {
|
||||||
|
Enabled: user.SafeZoneEnabled,
|
||||||
|
LastUpdate: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefaultRadius() {
|
||||||
|
return { Radius: 100 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { IsOptional, IsString, IsNumber, IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class NewShareDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
Email: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Start?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
End?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
Push?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShareListInDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
Active?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ShareListHistoryInDto {
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PutShareDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
Start?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
End?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoveShareDto {
|
||||||
|
@IsNumber()
|
||||||
|
ID: number;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
Push?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('shares')
|
||||||
|
export class Share {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
ID: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
DeviceID: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
ownerId: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
sharedWithEmail: string;
|
||||||
|
|
||||||
|
@Column({ default: true })
|
||||||
|
Active: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
Start: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
End: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
Name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
OriginalName: string;
|
||||||
|
|
||||||
|
@Column({ default: 'Me' })
|
||||||
|
Type: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Created: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Controller, Post, Put, Body, Param } from '@nestjs/common';
|
||||||
|
import { SharingService } from './sharing.service';
|
||||||
|
import { NewShareDto, ShareListInDto, ShareListHistoryInDto, PutShareDto, RemoveShareDto } from './dto/share.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('device/share')
|
||||||
|
export class SharingController {
|
||||||
|
constructor(private sharingService: SharingService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@CurrentUser() user: User, @Body() dto: NewShareDto) {
|
||||||
|
return this.sharingService.create(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('list')
|
||||||
|
list(@CurrentUser() user: User, @Body() dto: ShareListInDto) {
|
||||||
|
return this.sharingService.list(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('list/history')
|
||||||
|
listHistory(@CurrentUser() user: User, @Body() dto: ShareListHistoryInDto) {
|
||||||
|
return this.sharingService.listHistory(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
update(@CurrentUser() user: User, @Param('id') id: number, @Body() dto: PutShareDto) {
|
||||||
|
return this.sharingService.update(user.id, id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('remove')
|
||||||
|
remove(@CurrentUser() user: User, @Body() dto: RemoveShareDto) {
|
||||||
|
return this.sharingService.remove(user.id, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { SharingController } from './sharing.controller';
|
||||||
|
import { SharingService } from './sharing.service';
|
||||||
|
import { Share } from './entities/share.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Share])],
|
||||||
|
controllers: [SharingController],
|
||||||
|
providers: [SharingService],
|
||||||
|
})
|
||||||
|
export class SharingModule {}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Share } from './entities/share.entity';
|
||||||
|
import { NewShareDto, ShareListInDto, ShareListHistoryInDto, PutShareDto, RemoveShareDto } from './dto/share.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SharingService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Share)
|
||||||
|
private shareRepo: Repository<Share>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private mapShare(s: Share) {
|
||||||
|
return {
|
||||||
|
ID: s.ID,
|
||||||
|
DeviceID: s.DeviceID,
|
||||||
|
Active: s.Active,
|
||||||
|
Created: s.Created,
|
||||||
|
Start: s.Start,
|
||||||
|
End: s.End,
|
||||||
|
Name: s.Name || '',
|
||||||
|
OriginalName: s.OriginalName || '',
|
||||||
|
Type: s.Type,
|
||||||
|
ShareFrom: { ID: s.ownerId, Email: '' },
|
||||||
|
ShareTo: { ID: 0, Email: s.sharedWithEmail },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(userId: number, dto: NewShareDto) {
|
||||||
|
const share = this.shareRepo.create({
|
||||||
|
DeviceID: dto.DeviceID,
|
||||||
|
ownerId: userId,
|
||||||
|
sharedWithEmail: dto.Email,
|
||||||
|
Start: dto.Start,
|
||||||
|
End: dto.End,
|
||||||
|
Name: '',
|
||||||
|
OriginalName: '',
|
||||||
|
Type: 'Me',
|
||||||
|
});
|
||||||
|
const saved = await this.shareRepo.save(share);
|
||||||
|
return this.mapShare(saved);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list(userId: number, dto: ShareListInDto) {
|
||||||
|
const where: any = { DeviceID: dto.DeviceID, ownerId: userId };
|
||||||
|
if (dto.Active !== undefined) where.Active = dto.Active === 1;
|
||||||
|
if (dto.Type) where.Type = dto.Type;
|
||||||
|
const shares = await this.shareRepo.find({ where });
|
||||||
|
return shares.map((s) => this.mapShare(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
async listHistory(userId: number, dto: ShareListHistoryInDto) {
|
||||||
|
const where: any = { DeviceID: dto.DeviceID, ownerId: userId };
|
||||||
|
if (dto.Type) where.Type = dto.Type;
|
||||||
|
const shares = await this.shareRepo.find({ where });
|
||||||
|
return shares.map((s) => this.mapShare(s));
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(userId: number, id: number, dto: PutShareDto) {
|
||||||
|
const share = await this.shareRepo.findOne({ where: { ID: id, ownerId: userId } });
|
||||||
|
if (!share) throw new NotFoundException('Share not found');
|
||||||
|
if (dto.Name !== undefined) share.Name = dto.Name;
|
||||||
|
if (dto.Start !== undefined) share.Start = dto.Start;
|
||||||
|
if (dto.End !== undefined) share.End = dto.End;
|
||||||
|
await this.shareRepo.save(share);
|
||||||
|
return this.mapShare(share);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(userId: number, dto: RemoveShareDto): Promise<void> {
|
||||||
|
const result = await this.shareRepo.delete({ ID: dto.ID, DeviceID: dto.DeviceID, ownerId: userId });
|
||||||
|
if (result.affected === 0) throw new NotFoundException('Share not found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { IsEmail } from 'class-validator';
|
||||||
|
|
||||||
|
export class EmailModelDto {
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { IsOptional, IsString, IsBoolean } from 'class-validator';
|
||||||
|
|
||||||
|
export class UpdateUserDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
surname?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
password?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
TermsAndConditions?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
PrivacyPolicy?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('users')
|
||||||
|
export class User {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
password: string;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
surname: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
TermsAndConditions: boolean;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
PrivacyPolicy: boolean;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
facebook_id: number;
|
||||||
|
|
||||||
|
@Column({ default: true })
|
||||||
|
SafeZoneEnabled: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'datetime' })
|
||||||
|
Created: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { EmailModelDto } from './dto/email-model.dto';
|
||||||
|
import { CurrentUser } from '../common/decorators/current-user.decorator';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
|
||||||
|
@Controller('user')
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private usersService: UsersService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getProfile(@CurrentUser() user: User) {
|
||||||
|
return this.usersService.getProfile(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put()
|
||||||
|
updateProfile(@CurrentUser() user: User, @Body() dto: UpdateUserDto) {
|
||||||
|
return this.usersService.updateProfile(user.id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('account')
|
||||||
|
deleteAccount(@CurrentUser() user: User) {
|
||||||
|
return this.usersService.deleteAccount(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('backup')
|
||||||
|
getBackup() {
|
||||||
|
return { FileName: 'backup.json', Expire: new Date(Date.now() + 7 * 86400000).toISOString() };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('email')
|
||||||
|
changeEmail(@CurrentUser() user: User, @Body() dto: EmailModelDto) {
|
||||||
|
return this.usersService.changeEmail(user.id, dto);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { UsersController } from './users.controller';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User])],
|
||||||
|
controllers: [UsersController],
|
||||||
|
providers: [UsersService],
|
||||||
|
exports: [UsersService],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import * as bcrypt from 'bcrypt';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
import { UpdateUserDto } from './dto/update-user.dto';
|
||||||
|
import { EmailModelDto } from './dto/email-model.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private userRepo: Repository<User>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getProfile(userId: number) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { id: userId } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
return {
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
surname: user.surname,
|
||||||
|
TermsAndConditions: user.TermsAndConditions,
|
||||||
|
PrivacyPolicy: user.PrivacyPolicy,
|
||||||
|
Created: new Date(user.Created as any).toISOString(),
|
||||||
|
SafeZoneSettings: {
|
||||||
|
Enabled: user.SafeZoneEnabled,
|
||||||
|
LastUpdate: new Date(user.Created as any).toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateProfile(userId: number, dto: UpdateUserDto) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { id: userId } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
|
||||||
|
if (dto.name !== undefined) user.name = dto.name;
|
||||||
|
if (dto.surname !== undefined) user.surname = dto.surname;
|
||||||
|
if (dto.TermsAndConditions !== undefined)
|
||||||
|
user.TermsAndConditions = dto.TermsAndConditions;
|
||||||
|
if (dto.PrivacyPolicy !== undefined) user.PrivacyPolicy = dto.PrivacyPolicy;
|
||||||
|
if (dto.password) user.password = await bcrypt.hash(dto.password, 10);
|
||||||
|
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
return this.getProfile(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAccount(userId: number): Promise<void> {
|
||||||
|
const result = await this.userRepo.delete(userId);
|
||||||
|
if (result.affected === 0) throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
async changeEmail(userId: number, dto: EmailModelDto) {
|
||||||
|
const user = await this.userRepo.findOne({ where: { id: userId } });
|
||||||
|
if (!user) throw new NotFoundException('User not found');
|
||||||
|
user.email = dto.email;
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user