feat: auth

This commit is contained in:
2024-07-02 15:31:59 +03:00
parent 3c7b919b28
commit a865789efa
21 changed files with 342 additions and 201 deletions

View File

@@ -7,3 +7,5 @@ DATABASE_PORT=5432
SERVER_PORT=3000
REDIS_PASSWORD=123
JWT_SECRET=secret

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@
**/.env
**/.env.*
!**/.env.example
node_modules

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
dist

View File

@@ -1,9 +1,10 @@
{
"singleQuote": true,
"arrowParens": "always",
"printWidth": 100,
"singleQuote": false,
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"semi": true,
"tabWidth": 4,
"printWidth": 150,
"bracketSpacing": true,
"endOfLine": "lf"
"bracketSpacing": true
}

View File

@@ -10,6 +10,7 @@
"**/node_modules": true
},
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[dockercompose]": {
"editor.insertSpaces": true,

BIN
backend/bun.lockb Executable file

Binary file not shown.

View File

@@ -1,27 +1,30 @@
import { config as configInit } from 'dotenv';
import { config as configInit } from "dotenv";
configInit({ path: '../.env' });
configInit({ path: "../.env" });
export const config = {
database: {
type: 'postgres',
host: process.env.DATABASE_HOST || 'localhost',
type: "postgres",
host: process.env.DATABASE_HOST || "localhost",
port: +process.env.DATABASE_PORT || 5432,
username: process.env.DATABASE_USERNAME || 'postgres',
password: process.env.DATABASE_PASSWORD || '',
database: process.env.DATABASE_DB || 'bot_db',
username: process.env.DATABASE_USERNAME || "postgres",
password: process.env.DATABASE_PASSWORD || "",
database: process.env.DATABASE_DB || "bot_db",
synchronize: true,
logging: false,
autoLoadEntities: true,
},
redis: {
redis_host: process.env.REDIS_HOST || 'localhost',
redis_host: process.env.REDIS_HOST || "localhost",
redis_port: +process.env.REDIS_PORT || 6379,
redis_password: process.env.REDIS_PASSWORD || '',
redis_password: process.env.REDIS_PASSWORD || "",
redis_database: +process.env.REDIS_DB || 0,
},
server: {
port: +process.env.SERVER_PORT || 8080,
access_token: process.env.ACCESS_TOKEN || '',
access_token: process.env.ACCESS_TOKEN || "",
},
auth: {
jwt_secret: process.env.JWT_SECRET || "secret",
},
};

View File

@@ -0,0 +1,16 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class WebUser {
@PrimaryGeneratedColumn("uuid")
public uuid: string;
@Column({ unique: true })
public login: string;
@Column({ nullable: false })
public password: string;
@Column({ nullable: true })
public telegram_id?: string;
}

View File

@@ -1,14 +1,17 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Admin } from './database/admin.entity';
import { Image } from './database/image.entity';
import { Payment } from './database/payment.entity';
import { Post } from './database/post.entity';
import { ProxyUser } from './database/proxy_user.entity';
import { BotSettings } from './database/settings.entity';
import { User } from './database/user.entity';
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { Admin } from "./database/admin.entity";
import { Image } from "./database/image.entity";
import { Payment } from "./database/payment.entity";
import { Post } from "./database/post.entity";
import { ProxyUser } from "./database/proxy_user.entity";
import { BotSettings } from "./database/settings.entity";
import { User } from "./database/user.entity";
import { WebUser } from "./database/web_user.entity";
@Module({
imports: [TypeOrmModule.forFeature([User, Admin, Post, Image, Payment, ProxyUser, BotSettings])],
imports: [
TypeOrmModule.forFeature([User, Admin, Post, Image, Payment, ProxyUser, BotSettings, WebUser]),
],
exports: [TypeOrmModule],
})
export class LibsModule {}

View File

@@ -27,11 +27,12 @@
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.1.16",
"@nestjs/typeorm": "^10.0.1",
"bcrypt": "^5.1.1",
"cache-manager": "^5.4.0",
"cache-manager-redis-store": "^3.0.1",
"dotenv": "^16.3.1",
"passport": "^0.7.0",
"passport-http-bearer": "^1.0.1",
"passport-jwt": "^4.0.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
@@ -59,7 +60,8 @@
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
"typescript": "^5.1.3",
"@types/bun": "latest"
},
"jest": {
"moduleFileExtensions": [

View File

@@ -0,0 +1,48 @@
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Post,
Request,
UseGuards,
} from "@nestjs/common";
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
import { JwtGuard } from "./auth.guard";
import { AuthService } from "./auth.service";
import { LoginDto } from "./dto/login.dto";
@ApiTags("auth")
@Controller("auth")
export class AuthController {
constructor(private authService: AuthService) {}
@ApiOperation({
description: "Login into system",
})
@HttpCode(HttpStatus.OK)
@Post("login")
signIn(@Body() signInDto: LoginDto) {
return this.authService.signIn(signInDto.username, signInDto.password);
}
@ApiOperation({
description: "Register into system",
})
@HttpCode(HttpStatus.CREATED)
@Post("register")
register(@Body() signInDto: LoginDto) {
return this.authService.register(signInDto.username, signInDto.password);
}
@ApiOperation({
description: "Get user profile",
})
@UseGuards(JwtGuard)
@Get("profile")
@ApiBearerAuth()
getProfile(@Request() req) {
return req.user;
}
}

View File

@@ -1,36 +1,31 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthService } from './auth.service';
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private reflector: Reflector,
private readonly authService: AuthService,
) {}
export class JwtGuard extends AuthGuard("jwt") {}
// export class AuthGuard implements CanActivate {
// constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const allowUnauthorizedRequest = this.reflector.get<boolean>('allowUnauthorizedRequest', context.getHandler());
// async canActivate(context: ExecutionContext): Promise<boolean> {
// const request = context.switchToHttp().getRequest();
// const token = this.extractTokenFromHeader(request);
// if (!token) {
// throw new UnauthorizedException();
// }
// try {
// const payload = await this.jwtService.verifyAsync(token, {
// secret: config.auth.jwt_secret,
// });
let token = this.extractTokenFromHeader(request.headers);
// request["user"] = payload;
// } catch {
// throw new UnauthorizedException();
// }
if (!token) {
token = request.query.access_token || request.body.access_token;
}
if (allowUnauthorizedRequest || this.authService.authUserByToken(token)) return true;
throw new UnauthorizedException('Unathorized!');
}
// return true;
// }
private extractTokenFromHeader(headers: any): string | null {
if (headers && headers.authorization) {
const authHeader = headers.authorization as string;
const headerParts = authHeader.split(' ');
if (headerParts.length === 2 && headerParts[0].toLowerCase() === 'bearer') {
return headerParts[1];
}
}
return null;
}
}
// private extractTokenFromHeader(request: Request): string | undefined {
// const [type, token] = request.headers.authorization.split(" ") ?? [];
// return type === "Bearer" ? token : undefined;
// }
// }

View File

@@ -1,11 +1,24 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { HttpBearerStrategy } from './http-bearer.strategy';
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { LibsModule } from "libs/libs.module";
import { config } from "../../../config";
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./strategy/jwt.strategy";
@Module({
imports: [PassportModule.register({ defaultStrategy: 'bearer' })],
providers: [HttpBearerStrategy, AuthService],
exports: [HttpBearerStrategy, AuthService],
imports: [
LibsModule,
PassportModule,
JwtModule.register({
secret: config.auth.jwt_secret,
global: true,
signOptions: { expiresIn: 86000 },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -1,10 +1,51 @@
import { Injectable, Logger } from '@nestjs/common';
import { config } from 'config';
import { Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { InjectRepository } from "@nestjs/typeorm";
import * as bcrypt from "bcrypt";
import { WebUser } from "libs/database/web_user.entity";
import { Repository } from "typeorm";
@Injectable()
export class AuthService {
private readonly logger: Logger = new Logger(AuthService.name);
authUserByToken(token: string) {
return token === config.server.access_token;
constructor(
@InjectRepository(WebUser) private userRepository: Repository<WebUser>,
private jwtService: JwtService,
) {}
async signIn(username: string, pass: string): Promise<{ access_token: string }> {
const user = await this.userRepository.findOne({ where: { login: username } });
if (!user) throw new UnauthorizedException("User not found");
const isMatch = await bcrypt.compare(pass, user.password);
if (!isMatch) throw new UnauthorizedException("Wrong password");
const payload = { sub: user.uuid, username: user.login };
this.logger.log(`User ${user.login} logged in`);
return { access_token: await this.jwtService.signAsync(payload) };
}
async validateUserJwt(uuid: string): Promise<any> {
const user = await this.userRepository.findOneBy({ uuid: uuid });
if (!user) {
throw new UnauthorizedException("User not found");
}
return {
login: user.login,
telegram_id: user.telegram_id,
};
}
async register(username: string, password: string): Promise<{ access_token: string }> {
const user = await this.userRepository.findOne({ where: { login: username } });
if (user) throw new UnauthorizedException("User already exists");
const salt = await bcrypt.genSalt();
const hash = await bcrypt.hash(password, salt);
const newUser = this.userRepository.create({
login: username,
password: hash,
});
await this.userRepository.save(newUser);
this.logger.log(`User ${username} registered`);
const payload = { sub: newUser.uuid, username: newUser.login };
return { access_token: await this.jwtService.signAsync(payload) };
}
}

View File

@@ -0,0 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";
export class LoginDto {
@ApiProperty({ description: "User name", example: "test" }) readonly username: string;
@ApiProperty({ description: "User password", example: "test" }) readonly password: string;
}

View File

@@ -1,18 +0,0 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-http-bearer';
import { AuthService } from './auth.service';
@Injectable()
export class HttpBearerStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(token: string): Promise<boolean> {
const user = await this.authService.authUserByToken(token);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}

View File

@@ -0,0 +1,21 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { config } from "../../../../config";
import { AuthService } from "../auth.service";
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.auth.jwt_secret,
});
}
async validate(payload: { iat: number; sub: string }): Promise<any> {
const user = await this.authService.validateUserJwt(payload.sub);
if (!user) throw new UnauthorizedException();
return user;
}
}

View File

@@ -1,9 +1,14 @@
import { INestApplication } from '@nestjs/common';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { INestApplication } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
export function swagger(app: INestApplication): INestApplication {
const config = new DocumentBuilder().setTitle('Neuro website').setDescription('Some description').setVersion('0.1').build();
const config = new DocumentBuilder()
.setTitle("Neuro website")
.setDescription("Some description")
.setVersion("0.1")
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
SwaggerModule.setup("api", app, document);
return app;
}

BIN
bun.lockb Executable file

Binary file not shown.

View File

@@ -49,20 +49,19 @@ services:
depends_on:
- db
restart: always
bot:
container_name: neuro_bot
build: ./neuro-reply-bot-reworked
environment:
- API_URL=http://backend:3000
- TZ=Europe/Moscow
- REDIS_HOST=redis
networks:
- labnet
depends_on:
- backend
- redis
restart: always
# bot:
# container_name: neuro_bot
# build: ./neuro-reply-bot-reworked
# environment:
# - API_URL=http://backend:3000
# - TZ=Europe/Moscow
# - REDIS_HOST=redis
# networks:
# - labnet
# depends_on:
# - backend
# - redis
# restart: always
volumes:
neuro_postgres_db:

1
package.json Normal file
View File

@@ -0,0 +1 @@
{ "dependencies": { "@nestjs/jwt": "^10.2.0" } }