mirror of
https://github.com/jakobkordez/ham-reserve.git
synced 2025-08-05 12:47:41 +00:00
WIP
This commit is contained in:
@ -18,7 +18,7 @@ module.exports = {
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off'
|
||||
},
|
||||
|
@ -4,3 +4,53 @@
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Models
|
||||
|
||||
### User
|
||||
|
||||
```typescript
|
||||
class User {
|
||||
_id: string;
|
||||
username: string; // callsign
|
||||
password: string;
|
||||
roles: string[];
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
auth: string;
|
||||
createdAt: Date;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Event
|
||||
|
||||
```typescript
|
||||
class Event {
|
||||
_id: string;
|
||||
callsign: string;
|
||||
description: string;
|
||||
fromDateTime: Date;
|
||||
toDateTime: Date;
|
||||
access: User[];
|
||||
createdAt: Date;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### Reservation
|
||||
|
||||
```typescript
|
||||
class Reservation {
|
||||
_id: string;
|
||||
user: User;
|
||||
event: Event;
|
||||
fromDateTime: Date;
|
||||
toDateTime: Date;
|
||||
bands: string[];
|
||||
modes: string[];
|
||||
createdAt: Date;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
```
|
||||
|
@ -6,6 +6,7 @@ import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { APP_GUARD } from '@nestjs/core';
|
||||
import { AccessAuthGuard } from './auth/guards/access-auth.guard';
|
||||
import { RolesGuard } from './guards/roles.guard';
|
||||
import { EventsModule } from './events/events.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -24,6 +25,7 @@ import { RolesGuard } from './guards/roles.guard';
|
||||
|
||||
UsersModule,
|
||||
AuthModule,
|
||||
EventsModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
@ -5,6 +5,7 @@ import { RequestUser } from 'src/decorators/request-user.decorator';
|
||||
import { LocalAuthGuard } from './guards/local-auth.guard';
|
||||
import { RefreshAuthGuard } from './guards/refresh-auth.guard';
|
||||
import { UserTokenData } from './interfaces/user-token-data.interface';
|
||||
import { LoginResponse } from './interfaces/login-response.interface';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@ -13,14 +14,14 @@ export class AuthController {
|
||||
@Public()
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@Post('login')
|
||||
login(@RequestUser() user: UserTokenData) {
|
||||
login(@RequestUser() user: UserTokenData): Promise<LoginResponse> {
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@UseGuards(RefreshAuthGuard)
|
||||
@Get('refresh')
|
||||
refresh(@RequestUser() user: UserTokenData) {
|
||||
refresh(@RequestUser() user: UserTokenData): Promise<LoginResponse> {
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ import { JwtService } from '@nestjs/jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UserTokenData } from './interfaces/user-token-data.interface';
|
||||
import { isMongoId } from 'class-validator';
|
||||
import { LoginResponse } from './interfaces/login-response.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@ -30,7 +31,7 @@ export class AuthService {
|
||||
* @param user User to login
|
||||
* @returns Access and refresh tokens
|
||||
*/
|
||||
async login(user: UserTokenData) {
|
||||
async login(user: UserTokenData): Promise<LoginResponse> {
|
||||
const payload: UserTokenPayload = {
|
||||
username: user.username,
|
||||
sub: user.id,
|
||||
@ -57,7 +58,7 @@ export class AuthService {
|
||||
* Sets refresh token to `null`
|
||||
* @param userId User to logout
|
||||
*/
|
||||
async logout(userId: string) {
|
||||
async logout(userId: string): Promise<void> {
|
||||
await this.usersService.setRefreshToken(userId, null);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Observable } from 'rxjs';
|
||||
import { IS_PUBLIC_KEY } from 'src/decorators/public.decorator';
|
||||
|
||||
@Injectable()
|
||||
@ -9,7 +10,9 @@ export class AccessAuthGuard extends AuthGuard('jwt') {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
|
4
nest-api/src/auth/interfaces/login-response.interface.ts
Normal file
4
nest-api/src/auth/interfaces/login-response.interface.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface LoginResponse {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
@ -2,6 +2,7 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UserTokenData } from '../interfaces/user-token-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
@ -13,7 +14,7 @@ export class JwtAccessStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
});
|
||||
}
|
||||
|
||||
async validate(payload: any) {
|
||||
async validate(payload: any): Promise<UserTokenData> {
|
||||
return { id: payload.sub, username: payload.username };
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Request } from 'express';
|
||||
import { UserTokenPayload } from '../interfaces/user-token-payload.interface';
|
||||
import { UserTokenData } from '../interfaces/user-token-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class JwtRefreshStrategy extends PassportStrategy(
|
||||
@ -23,7 +24,10 @@ export class JwtRefreshStrategy extends PassportStrategy(
|
||||
});
|
||||
}
|
||||
|
||||
async validate(req: Request, payload: UserTokenPayload) {
|
||||
async validate(
|
||||
req: Request,
|
||||
payload: UserTokenPayload,
|
||||
): Promise<UserTokenData> {
|
||||
const isValid = await this.authService.validateRefreshToken(
|
||||
ExtractJwt.fromAuthHeaderAsBearerToken()(req),
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { CustomDecorator, SetMetadata } from '@nestjs/common';
|
||||
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
export const Public = (): CustomDecorator => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { CustomDecorator, SetMetadata } from '@nestjs/common';
|
||||
import { Role } from '../enums/role.enum';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
|
||||
export const Roles = (...roles: Role[]): CustomDecorator =>
|
||||
SetMetadata(ROLES_KEY, roles);
|
||||
|
17
nest-api/src/events/dto/create-event.dto.ts
Normal file
17
nest-api/src/events/dto/create-event.dto.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { IsDate, IsOptional, IsString, MinLength } from 'class-validator';
|
||||
|
||||
export class CreateEventDto {
|
||||
@IsString()
|
||||
@MinLength(3)
|
||||
callsign: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description: string;
|
||||
|
||||
@IsDate()
|
||||
fromDateTime: Date;
|
||||
|
||||
@IsDate()
|
||||
toDateTime: Date;
|
||||
}
|
4
nest-api/src/events/dto/update-event.dto.ts
Normal file
4
nest-api/src/events/dto/update-event.dto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateEventDto } from './create-event.dto';
|
||||
|
||||
export class UpdateEventDto extends PartialType(CreateEventDto) {}
|
20
nest-api/src/events/events.controller.spec.ts
Normal file
20
nest-api/src/events/events.controller.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { EventsController } from './events.controller';
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
describe('EventsController', () => {
|
||||
let controller: EventsController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [EventsController],
|
||||
providers: [EventsService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<EventsController>(EventsController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
58
nest-api/src/events/events.controller.ts
Normal file
58
nest-api/src/events/events.controller.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
} from '@nestjs/common';
|
||||
import { EventsService } from './events.service';
|
||||
import { CreateEventDto } from './dto/create-event.dto';
|
||||
import { UpdateEventDto } from './dto/update-event.dto';
|
||||
import { MongoIdPipe } from 'src/pipes/mongo-id.pipe';
|
||||
import { Event } from './schemas/event.schema';
|
||||
import { Roles } from 'src/decorators/roles.decorator';
|
||||
import { Role } from 'src/enums/role.enum';
|
||||
|
||||
@Controller('events')
|
||||
export class EventsController {
|
||||
constructor(private readonly eventsService: EventsService) {}
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Post()
|
||||
create(@Body() createEventDto: CreateEventDto): Promise<Event> {
|
||||
return this.eventsService.create(createEventDto);
|
||||
}
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Get('all')
|
||||
findAll(): Promise<Event[]> {
|
||||
return this.eventsService.findAll();
|
||||
}
|
||||
|
||||
@Get()
|
||||
findCurrent(): Promise<Event[]> {
|
||||
return this.eventsService.findCurrent();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', MongoIdPipe) id: string): Promise<Event> {
|
||||
return this.eventsService.findOne(id);
|
||||
}
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Patch(':id')
|
||||
update(
|
||||
@Param('id', MongoIdPipe) id: string,
|
||||
@Body() updateEventDto: UpdateEventDto,
|
||||
): Promise<Event> {
|
||||
return this.eventsService.update(id, updateEventDto);
|
||||
}
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Delete(':id')
|
||||
remove(@Param('id', MongoIdPipe) id: string): Promise<Event> {
|
||||
return this.eventsService.setDeleted(id);
|
||||
}
|
||||
}
|
14
nest-api/src/events/events.module.ts
Normal file
14
nest-api/src/events/events.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { EventsService } from './events.service';
|
||||
import { EventsController } from './events.controller';
|
||||
import { Event, EventSchema } from './schemas/event.schema';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
MongooseModule.forFeature([{ name: Event.name, schema: EventSchema }]),
|
||||
],
|
||||
controllers: [EventsController],
|
||||
providers: [EventsService],
|
||||
})
|
||||
export class EventsModule {}
|
18
nest-api/src/events/events.service.spec.ts
Normal file
18
nest-api/src/events/events.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { EventsService } from './events.service';
|
||||
|
||||
describe('EventsService', () => {
|
||||
let service: EventsService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [EventsService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EventsService>(EventsService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
47
nest-api/src/events/events.service.ts
Normal file
47
nest-api/src/events/events.service.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateEventDto } from './dto/create-event.dto';
|
||||
import { UpdateEventDto } from './dto/update-event.dto';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Event } from './schemas/event.schema';
|
||||
|
||||
@Injectable()
|
||||
export class EventsService {
|
||||
constructor(
|
||||
@InjectModel(Event.name) private readonly eventModel: Model<Event>,
|
||||
) {}
|
||||
|
||||
create(createEventDto: CreateEventDto): Promise<Event> {
|
||||
const event = new this.eventModel(createEventDto);
|
||||
return event.save();
|
||||
}
|
||||
|
||||
findAll(): Promise<Event[]> {
|
||||
return this.eventModel.find().exec();
|
||||
}
|
||||
|
||||
findCurrent(): Promise<Event[]> {
|
||||
const now = new Date();
|
||||
return this.eventModel
|
||||
.find({
|
||||
$and: [{ fromDateTime: { $lte: now } }, { toDateTime: { $gte: now } }],
|
||||
})
|
||||
.exec();
|
||||
}
|
||||
|
||||
findOne(id: string): Promise<Event> {
|
||||
return this.eventModel.findById(id).exec();
|
||||
}
|
||||
|
||||
update(id: string, updateEventDto: UpdateEventDto): Promise<Event> {
|
||||
return this.eventModel
|
||||
.findByIdAndUpdate(id, updateEventDto, { new: true })
|
||||
.exec();
|
||||
}
|
||||
|
||||
setDeleted(id: string): Promise<Event> {
|
||||
return this.eventModel
|
||||
.findByIdAndUpdate(id, { $set: { isDeleted: true } }, { new: true })
|
||||
.exec();
|
||||
}
|
||||
}
|
41
nest-api/src/events/schemas/event.schema.ts
Normal file
41
nest-api/src/events/schemas/event.schema.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { User } from 'src/users/schemas/user.schema';
|
||||
|
||||
export type EventDocument = Event & Document;
|
||||
|
||||
@Schema()
|
||||
export class Event {
|
||||
_id: string;
|
||||
|
||||
@Prop({ required: true })
|
||||
callsign: string;
|
||||
|
||||
@Prop()
|
||||
description: string;
|
||||
|
||||
@Prop()
|
||||
fromDateTime: Date;
|
||||
|
||||
@Prop()
|
||||
toDateTime: Date;
|
||||
|
||||
@Prop({
|
||||
type: [{ type: String, ref: User.name }],
|
||||
default: [],
|
||||
})
|
||||
access: User[];
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
default: Date.now,
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
default: false,
|
||||
})
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
export const EventSchema = SchemaFactory.createForClass(Event);
|
@ -3,7 +3,7 @@ import { AppModule } from './app.module';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import helmet from 'helmet';
|
||||
|
||||
async function bootstrap() {
|
||||
async function bootstrap(): Promise<void> {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.enableCors();
|
||||
|
@ -3,7 +3,7 @@ import { isValidObjectId } from 'mongoose';
|
||||
|
||||
@Injectable()
|
||||
export class MongoIdPipe implements PipeTransform {
|
||||
transform(value: any) {
|
||||
transform(value: any): string {
|
||||
if (!isValidObjectId(value)) throw new BadRequestException('Invalid id');
|
||||
|
||||
return value;
|
||||
|
@ -41,6 +41,18 @@ export class User {
|
||||
set: (value: string) => bcrypt.hashSync(value, 10),
|
||||
})
|
||||
auth: string;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
default: Date.now,
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
default: false,
|
||||
})
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
export const UserSchema = SchemaFactory.createForClass(User);
|
||||
|
@ -14,42 +14,42 @@ import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { MongoIdPipe } from 'src/pipes/mongo-id.pipe';
|
||||
import { Role } from 'src/enums/role.enum';
|
||||
import { Roles } from 'src/decorators/roles.decorator';
|
||||
import { Public } from 'src/decorators/public.decorator';
|
||||
import { RequestUser } from 'src/decorators/request-user.decorator';
|
||||
import { UserTokenData } from 'src/auth/interfaces/user-token-data.interface';
|
||||
import { User } from './schemas/user.schema';
|
||||
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Public()
|
||||
@Post()
|
||||
create(@Body() createUserDto: CreateUserDto) {
|
||||
create(@Body() createUserDto: CreateUserDto): Promise<User> {
|
||||
return this.usersService.create(createUserDto);
|
||||
}
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Get()
|
||||
findAll() {
|
||||
findAll(): Promise<User[]> {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
getSelf(@RequestUser() userReq: UserTokenData) {
|
||||
getSelf(@RequestUser() userReq: UserTokenData): Promise<User> {
|
||||
const user = this.usersService.findOne(userReq.id);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
return user;
|
||||
}
|
||||
|
||||
@Get('search/:username')
|
||||
findByUsername(@Param('username') username: string) {
|
||||
findByUsername(@Param('username') username: string): Promise<User> {
|
||||
const user = this.usersService.findByUsername(username);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
return user;
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id', MongoIdPipe) id: string) {
|
||||
findOne(@Param('id', MongoIdPipe) id: string): Promise<User> {
|
||||
return this.usersService.findOne(id);
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ export class UsersController {
|
||||
update(
|
||||
@Param('id', MongoIdPipe) id: string,
|
||||
@Body() updateUserDto: UpdateUserDto,
|
||||
) {
|
||||
): Promise<User> {
|
||||
const user = this.usersService.update(id, updateUserDto);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
return user;
|
||||
@ -66,8 +66,8 @@ export class UsersController {
|
||||
|
||||
@Roles(Role.Admin)
|
||||
@Delete(':id')
|
||||
remove(@Param('id', MongoIdPipe) id: string) {
|
||||
const user = this.usersService.remove(id);
|
||||
async remove(@Param('id', MongoIdPipe) id: string): Promise<void> {
|
||||
const user = this.usersService.setDeleted(id);
|
||||
if (!user) throw new NotFoundException('User not found');
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class UsersService {
|
||||
}
|
||||
|
||||
findAll(): Promise<User[]> {
|
||||
return this.userModel.find().exec();
|
||||
return this.userModel.find({ isDeleted: { $in: [false, null] } }).exec();
|
||||
}
|
||||
|
||||
findOne(id: string): Promise<User> {
|
||||
@ -34,8 +34,10 @@ export class UsersService {
|
||||
.exec();
|
||||
}
|
||||
|
||||
remove(id: string): Promise<User> {
|
||||
return this.userModel.findByIdAndDelete(id).exec();
|
||||
setDeleted(id: string): Promise<User> {
|
||||
return this.userModel
|
||||
.findByIdAndUpdate(id, { $set: { isDeleted: true } }, { new: true })
|
||||
.exec();
|
||||
}
|
||||
|
||||
setRefreshToken(id: string, token: string): Promise<User> {
|
||||
|
Reference in New Issue
Block a user