Skip to content

Commit

Permalink
feat: admin endpoint and auth fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
alerdenisov committed Oct 27, 2023
1 parent c3f5013 commit 4061582
Show file tree
Hide file tree
Showing 18 changed files with 183 additions and 104 deletions.
25 changes: 23 additions & 2 deletions apps/api/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ClassSerializerInterceptor, Controller, Post, UseInterceptors } from '@nestjs/common';
import { ClassSerializerInterceptor, Controller, Get, Param, ParseIntPipe, Post, UseInterceptors } from '@nestjs/common';
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';

import { User } from '@tookey/database';
import { AuthTokenDto } from '../auth/auth.dto';
import { AnyRoles } from '../decorators/any-role.decorator';
import { CurrentUser } from '../decorators/current-user.decorator';
import { JwtAuth } from '../decorators/jwt-auth.decorator';
import { UserContextDto, UserDto } from '../user/user.dto';
import { AdminService } from './admin.service';
import { AnyRoles } from '../decorators/any-role.decorator';

@Controller('api/admin')
@ApiTags('Admin')
Expand All @@ -22,4 +24,23 @@ export class AdminController {
async removeUserData(@CurrentUser() user: UserContextDto): Promise<UserDto> {
return this.adminService.removeUserData(user.id);
}


@AnyRoles('admin', 'assistant')
@ApiOperation({ description: 'Returns list of all registered users' })
@ApiOkResponse({ type: User, isArray: true })
@ApiNotFoundResponse()
@Get('users')
async getUsers(): Promise<User[]> {
return this.adminService.getAllUsers();
}

@AnyRoles('admin')
@ApiOperation({ description: 'Creates one time password to auth in Automation' })
@ApiOkResponse({ type: AuthTokenDto })
@ApiNotFoundResponse()
@Get('user/:id/otp')
async getUserOtp(@Param('id', ParseIntPipe) userId: number) {
return this.adminService.getUserOtp(userId);
}
}
3 changes: 2 additions & 1 deletion apps/api/src/admin/admin.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { AccessModule } from '@tookey/access';
import {
AofgProfileRepository,
KeyRepository,
Expand All @@ -21,7 +22,7 @@ const AdminRepositories = TypeOrmExModule.forCustomRepository([
]);

@Module({
imports: [AdminRepositories, UserModule],
imports: [AdminRepositories, UserModule, AccessModule],
controllers: [AdminController],
providers: [AdminService],
})
Expand Down
13 changes: 12 additions & 1 deletion apps/api/src/admin/admin.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { Injectable } from '@nestjs/common';
import { AofgProfileRepository, KeyRepository, SignRepository, UserDiscordRepository } from '@tookey/database';
import { AccessService } from '@tookey/access';
import { AofgProfileRepository, KeyRepository, SignRepository, User, UserDiscordRepository } from '@tookey/database';
import { AuthTokenDto } from '../auth/auth.dto';

import { UserDto } from '../user/user.dto';
import { UserService } from '../user/user.service';

@Injectable()
export class AdminService {
getUserOtp(userId: number) {
return this.accessService.getAccessToken(userId);
}

constructor(
private readonly userService: UserService,
private readonly signs: SignRepository,
private readonly keys: KeyRepository,
private readonly userDiscord: UserDiscordRepository,
private readonly aofgProfile: AofgProfileRepository,
private readonly accessService: AccessService
) {}

async removeUserData(id: number): Promise<UserDto | null> {
Expand All @@ -29,4 +36,8 @@ export class AdminService {
}
}
}

async getAllUsers(): Promise<User[]> {
return this.userService.getAllUsers();
}
}
4 changes: 2 additions & 2 deletions apps/api/src/auth-discord/auth-discord.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ export class AuthDiscordController {
console.log('uncached auth')
const profile = await this.discordService.getProfileByCode(body);
const user = await this.userService.getOrCreateDiscordUser(new CreateDiscordUserDto(profile));
const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'google'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'google'] });
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);
await this.userService.setCurrentRefreshToken(refresh.token, user.userId);
return { access, refresh, user: user.user };
}
Expand Down
8 changes: 4 additions & 4 deletions apps/api/src/auth-email/auth-email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ export class AuthEmailService {
if (!user.verified) throw new BadRequestException('User not verified');
if (!isValidPassword) throw new BadRequestException('Invalid password');

const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'email'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'email'] });
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);
await this.userService.setCurrentRefreshToken(refresh.token, user.userId);
return { access, refresh, user: user.user };
}
Expand Down Expand Up @@ -104,8 +104,8 @@ export class AuthEmailService {

user.password = password;

const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'email'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'email'] });
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);
await this.userService.setCurrentRefreshToken(refresh.token, user.userId);

await this.userEmailRepository.save(user)
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/auth-google/auth-google.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export class AuthGoogleController {
async googleAuthCallback(@Body() body: GoogleAuthLoginDto) {
const profile = await this.googleService.getProfileByToken(body);
const user = await this.userService.getOrCreateGoogleUser(profile);
const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'google'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'google'] });
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);
await this.userService.setCurrentRefreshToken(refresh.token, user.userId);
return { access, refresh, user: user.user };
// return { access, refresh };
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/auth-twitter/auth-twitter.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ export class AuthTwitterController {
console.log('uncached auth')
const profile = await this.twitterService.getProfileByCode(body);
const user = await this.userService.getOrCreateTwitterUser(new CreateTwitterUserDto(profile));
const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'google'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'google'] });
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);
await this.userService.setCurrentRefreshToken(refresh.token, user.userId);
return { access, refresh, user: user.user };
}
Expand Down
72 changes: 35 additions & 37 deletions apps/api/src/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,26 +69,21 @@ export class AuthController {
@HttpCode(200)
@Post('flows')
async signinFlow(@CurrentUser() userDto: UserContextDto) {
const user = await this.userService.getUser({ id: userDto.id });
if (!user) throw new NotFoundException('Telegram user not found');

console.log('currentuser', userDto)
const userRequest = {
id: user.id.toString(),
firstName: user.firstName,
lastName: user.lastName,
id: userDto.user.id.toString(),
firstName: userDto.user.firstName,
lastName: userDto.user.lastName,
trackEvents: true,
newsLetter: true,
};
await this.flowsService.injectUser(userRequest);
const flowUser = await this.flowsService.authUser({
id: userRequest.id,
token: this.authService.getJwtServiceToken({
id: user.id,
roles: ['user', 'flows'],
}).token,
token: this.authService.getJwtServiceToken(userDto.user).token,
});

this.eventEmitter.emit(AuthEvent.SIGNIN, user.id, 'Automation');
this.eventEmitter.emit(AuthEvent.SIGNIN, userDto.user.id, 'Automation');
return flowUser;
}

Expand All @@ -98,8 +93,8 @@ export class AuthController {
@HttpCode(200)
@Post('signin')
async signin(@CurrentUser() user: UserContextDto): Promise<AuthTokensResponseDto> {
const access = this.authService.getJwtAccessToken({ id: user.id, roles: ['user', 'otp'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.id, roles: ['user', 'otp'] });
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);

await this.userService.setCurrentRefreshToken(refresh.token, user.id);

Expand All @@ -115,32 +110,35 @@ export class AuthController {
@ApiOkResponse({ type: AuthTokenDto })
@HttpCode(200)
@Post('refresh')
refresh(@CurrentUser() user: UserContextDto): AuthTokenDto {
return this.authService.getJwtAccessToken({ id: user.id, roles: ['user', 'discord'] });
async refresh(@CurrentUser() user: UserContextDto): Promise<AuthTokensResponseDto> {
const access = this.authService.getJwtAccessToken(user.user);
const refresh = this.authService.getJwtRefreshToken(user.user);
await this.userService.setCurrentRefreshToken(refresh.token, user.id);
return { access, refresh , user: user.user as any };
}

@ApiOperation({ description: 'Get twitter auth url' })
@ApiOkResponse({ type: TwitterAuthUrlResponseDto })
@Get('twitter')
async twitterAuthUrl(): Promise<TwitterAuthUrlResponseDto> {
const { url, state, codeVerifier } = await this.twitterService.getAuthLink();
this.twitterService.saveSession(state, codeVerifier);
return { url };
}
// @ApiOperation({ description: 'Get twitter auth url' })
// @ApiOkResponse({ type: TwitterAuthUrlResponseDto })
// @Get('twitter')
// async twitterAuthUrl(): Promise<TwitterAuthUrlResponseDto> {
// const { url, state, codeVerifier } = await this.twitterService.getAuthLink();
// this.twitterService.saveSession(state, codeVerifier);
// return { url };
// }

@ApiOperation({ description: 'Get access and refresh tokens with twitter' })
@ApiOkResponse({ type: AuthTokensResponseDto })
@Post('twitter')
async twitterAuthCallback(@Body() body: AuthTwitterCallbackDto): Promise<AuthTokensResponseDto> {
const session = await this.twitterService.loadSession(body.state);
if (!session) throw new BadRequestException('Session not found');

const user = await this.twitterService.requestUser({ code: body.code, codeVerifier: session.codeVerifier });
const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'twitter'] });
const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'twitter'] });
await this.userService.setCurrentRefreshToken(refresh.token, user.id);
return { access, refresh, user: user.user as any };
}
// @ApiOperation({ description: 'Get access and refresh tokens with twitter' })
// @ApiOkResponse({ type: AuthTokensResponseDto })
// @Post('twitter')
// async twitterAuthCallback(@Body() body: AuthTwitterCallbackDto): Promise<AuthTokensResponseDto> {
// const session = await this.twitterService.loadSession(body.state);
// if (!session) throw new BadRequestException('Session not found');

// const user = await this.twitterService.requestUser({ code: body.code, codeVerifier: session.codeVerifier });
// const access = this.authService.getJwtAccessToken({ id: user.userId, roles: ['user', 'twitter'] });
// const refresh = this.authService.getJwtRefreshToken({ id: user.userId, roles: ['user', 'twitter'] });
// await this.userService.setCurrentRefreshToken(refresh.token, user.id);
// return { access, refresh, user: user.user as any };
// }

// @ApiOperation({ description: 'Get discord auth url' })
// @ApiOkResponse({ type: DiscordAuthUrlResponseDto })
Expand All @@ -165,7 +163,7 @@ export class AuthController {
// }

@ApiOperation({ description: 'Get OTP token to connect external service' })
@ApiOkResponse({ type: AuthTokensResponseDto })
@ApiOkResponse({ type: AuthTokenDto })
@JwtAuth()
@Get('access')
async getAccessToken(@CurrentUser() user: UserContextDto) {
Expand Down
13 changes: 7 additions & 6 deletions apps/api/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { JwtService } from '@nestjs/jwt';

import { AuthTokenDto, PricipalDto } from './auth.dto';
import { classToPlain, instanceToPlain, plainToInstance } from 'class-transformer';
import { User } from '@tookey/database';

@Injectable()
export class AuthService {
Expand Down Expand Up @@ -52,24 +53,24 @@ export class AuthService {
return this.getJwtToken({ id: -1, roles: ['admin'] }, jwt.secret, 60 * 60 * 24 * 365); // 1 year
}

public getJwtServiceToken(principal: PricipalDto) {
const plain = instanceToPlain(principal) as PricipalDto;
public getJwtServiceToken(user: User) {
const plain = user.toPrincipal()
plain.roles.push('service');
const jwt = this.configService.get('jwt', { infer: true });
if (!jwt) throw new InternalServerErrorException('Invalid JWT Access Token configuration');
return this.getJwtToken(plain, jwt.secret, 60 * 60 * 24 * 365); // 1 year
}

public getJwtAccessToken(principal: PricipalDto) {
const plain = instanceToPlain(principal) as PricipalDto;
public getJwtAccessToken(user: User) {
const plain = user.toPrincipal()
plain.roles.push('access');
const jwt = this.configService.get('jwt', { infer: true });
if (!jwt) throw new InternalServerErrorException('Invalid JWT Access Token configuration');
return this.getJwtToken(plain, jwt.secret, jwt.accessTokenTTL);
}

public getJwtRefreshToken(principal: PricipalDto) {
const plain = instanceToPlain(principal) as PricipalDto;
public getJwtRefreshToken(user: User) {
const plain = user.toPrincipal()
plain.roles.push('refresh');
const jwt = this.configService.get('jwt', { infer: true });
if (!jwt) throw new InternalServerErrorException('Invalid JWT Refresh Token configuration');
Expand Down
22 changes: 15 additions & 7 deletions apps/api/src/auth/strategies/jwt.strategy.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { AppConfiguration } from 'apps/app/src/app.config';
import { ExtractJwt, Strategy } from 'passport-jwt';

import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';

import { UserService } from '../../user/user.service';
import { PricipalDto } from '../auth.dto';
import { UserContextDto } from '../../user/user.dto';
import { plainToInstance } from 'class-transformer';
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
import { UserContextDto } from '../../user/user.dto';
import { UserService } from '../../user/user.service';
import { PricipalDto } from '../auth.dto';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
Expand All @@ -22,12 +22,20 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwt.secret,
}, async (payload: PricipalDto, done: (err: Error | null, user?: UserContextDto) => void) => {
try {
const user = await this.verifyPrincipal(payload);
done(null, user);
} catch (err) {
done(err);
}
});
}

async validate(payload: PricipalDto) {
const user = this.userService.getUser({ id: payload.id });
this.logger.info('roles', { roles: payload.roles })
private async verifyPrincipal(payload: PricipalDto) {
const user = await this.userService.getUser({ id: payload.id });
if (!user) throw new NotFoundException('User not found');
this.logger.info(`roles: ${user.toRoles()}, payload: ${JSON.stringify(payload)}`)

return plainToInstance(UserContextDto, {
...payload,
Expand Down
9 changes: 5 additions & 4 deletions apps/api/src/auth/strategies/signin-key.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ export class SigninKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, 's
constructor(readonly accessService: AccessService, readonly userService: UserService) {
super(
{ header: 'X-SIGNIN-KEY' },
true,
async (apikey: string, done: (err: Error | null, user?: UserContextDto) => void) => {
false,
async (apikey: string, verified: (err: Error | null, user?: UserContextDto) => void) => {
const userId = await accessService.getTokenUserId(apikey);
const user = await userService.getUser({ id: userId });
if (!userId) return done(new UnauthorizedException('Token is not valid'));
return done(null, { id: userId, user, roles: [] });
if (!userId) return verified(new UnauthorizedException('Token is not valid'));
// return verified(null, { foo: 'bar' });
return verified(null, { id: userId, user, roles: user.toRoles() });
},
);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/decorators/current-user.decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { ExecutionContext, createParamDecorator } from '@nestjs/common';

export const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user.user;
return request.user;
});

export const WsCurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.handshake.user.user;
return request.handshake.user;
});
7 changes: 4 additions & 3 deletions apps/api/src/user/user.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { formatISO } from 'date-fns';

import { ApiProperty, ApiPropertyOptional, OmitType, PartialType } from '@nestjs/swagger';
import { UserRole } from '@tookey/database/entities/user-role.type';
import { User } from '@tookey/database';


@Exclude()
Expand Down Expand Up @@ -48,10 +49,10 @@ export class UserContextDto {
@IsNumber()
id: number;

@ApiProperty({ type: () => UserDto })
@ApiProperty({ type: () => User })
@ValidateNested()
@Type(() => UserDto)
user: UserDto;
@Type(() => User)
user: User;

@ApiPropertyOptional()
@IsString({ each: true })
Expand Down
Loading

0 comments on commit 4061582

Please sign in to comment.