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