Skip to content

Commit

Permalink
feat(be): add like chapter api
Browse files Browse the repository at this point in the history
  • Loading branch information
harisato committed Jul 17, 2023
1 parent 76d6c7c commit 27dadaf
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 25 deletions.
14 changes: 5 additions & 9 deletions src/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,11 @@ export class AuthGuard implements CanActivate {
'x-hasura-user-id': userId,
'x-hasura-allowed-roles': allowedRoles,
} = payload['https://hasura.io/jwt/claims'];
if (allowedRoles.includes('admin')) {
request['user'] = {
userId,
allowedRoles,
token,
};
} else {
throw new UnauthorizedException();
}
request['user'] = {
userId,
roles: allowedRoles,
token,
};
} catch (e) {
console.log(e);
throw new UnauthorizedException();
Expand Down
2 changes: 1 addition & 1 deletion src/auth/dto/user.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export class UserDto {
userId: string;
allowedRoles: string[];
roles: string[];
token: string;
}
4 changes: 4 additions & 0 deletions src/auth/role.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Role {
User = 'user',
Admin = 'admin',
}
22 changes: 22 additions & 0 deletions src/auth/role.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './role.enum';
import { ROLES_KEY } from './roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();

return requiredRoles.some((role) => user.roles?.includes(role));
}
}
5 changes: 5 additions & 0 deletions src/auth/roles.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
9 changes: 7 additions & 2 deletions src/chapter/chapter.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,30 @@ import {
UpdateChapterRequestDto,
} from './dto/update-chapter-request.dto';
import { IncreaseChapterViewParamDto } from './dto/increase-chapter-view-request.dto';
import { Role } from '../auth/role.enum';
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/role.guard';

@Controller('chapter')
export class ChapterController {
constructor(private readonly chapterSvc: ChapterService) {}

@UseGuards(AuthGuard)
@UseGuards(AuthGuard, RolesGuard)
@ApiBearerAuth()
@Post()
@ApiConsumes('multipart/form-data')
@UseInterceptors(AuthUserInterceptor, AnyFilesInterceptor())
@Roles(Role.Admin)
create(
@Body() data: CreateChapterRequestDto,
@UploadedFiles() files: Array<Express.Multer.File>,
) {
return this.chapterSvc.create(data, files);
}

@UseGuards(AuthGuard)
@UseGuards(AuthGuard, RolesGuard)
@ApiBearerAuth()
@Roles(Role.Admin)
@Put(':chapterId')
@ApiConsumes('multipart/form-data')
@UseInterceptors(AuthUserInterceptor, AnyFilesInterceptor())
Expand Down
16 changes: 14 additions & 2 deletions src/chapter/chapter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,25 @@ export class ChapterService {

// set chapter to set
this.redisClientService.client.sAdd(
['punkga', 'dev', 'chapters'].join(':'),
[
this.configService.get<string>('app.name'),
this.configService.get<string>('app.env'),
,
'chapters',
].join(':'),
chapterId.toString(),
);

// increase
this.redisClientService.client.incr(
['punkga', 'dev', 'chapter', chapterId.toString(), 'view'].join(':'),
[
this.configService.get<string>('app.name'),
this.configService.get<string>('app.env'),
,
'chapter',
chapterId.toString(),
'view',
].join(':'),
);
return {
success: true,
Expand Down
9 changes: 7 additions & 2 deletions src/manga/manga.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ import {
UpdateMangaParamDto,
UpdateMangaRequestDto,
} from './dto/update-manga-request.dto';
import { Roles } from '../auth/roles.decorator';
import { Role } from '../auth/role.enum';
import { RolesGuard } from '../auth/role.guard';

@Controller('manga')
export class MangaController {
constructor(private readonly mangaSvc: MangaService) {}

@UseGuards(AuthGuard)
@UseGuards(AuthGuard, RolesGuard)
@ApiBearerAuth()
@Roles(Role.Admin)
@Post()
@ApiConsumes('multipart/form-data')
@UseInterceptors(AuthUserInterceptor, AnyFilesInterceptor())
Expand All @@ -36,8 +40,9 @@ export class MangaController {
return this.mangaSvc.create(data, files);
}

@UseGuards(AuthGuard)
@UseGuards(AuthGuard, RolesGuard)
@ApiBearerAuth()
@Roles(Role.Admin)
@Put(':mangaId')
@ApiConsumes('multipart/form-data')
@UseInterceptors(AuthUserInterceptor, AnyFilesInterceptor())
Expand Down
65 changes: 64 additions & 1 deletion src/task/task.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class TasksService {
) {}

@Cron(CronExpression.EVERY_5_MINUTES)
async updateViews() {
async updateChapterViews() {
// set chapter to set
const chapters = await this.redisClientService.client.sPop(
[
Expand Down Expand Up @@ -77,4 +77,67 @@ export class TasksService {
this.logger.debug('Update chapter views', updates);
}
}

@Cron(CronExpression.EVERY_5_SECONDS)
async updateChapterLikes() {
// set chapter to set
const chapter = await this.redisClientService.client.sPop(
[
this.configService.get<string>('app.name'),
this.configService.get<string>('app.env'),
'chapter-likes',
].join(':'),
10,
);
if (chapter.length > 0) {
const likes = await Promise.all(
chapter.map((chapterId: string) => {
// get chapter view
const key = [
this.configService.get<string>('app.name'),
this.configService.get<string>('app.env'),
'chapters',
chapterId.toString(),
'like',
].join(':');
return this.redisClientService.client.getDel(key);
}),
);

const updates = chapter
.map((chapterId: string, index: number) => ({
where: {
id: {
_eq: chapterId,
},
},
_inc: {
likes: Number(likes[index]),
},
}))
.filter((u) => u._inc.likes !== 0);

const updateResult = await this.graphqlSvc.query(
this.configService.get<string>('graphql.endpoint'),
'',
`mutation UpdateChapterLikes($updates: [chapters_updates!] = {where: {id: {_eq: 10}}, _inc: {likes: 10}}) {
update_chapters_many(updates: $updates) {
affected_rows
}
}`,
'UpdateChapterLikes',
{
updates,
},
{
'x-hasura-admin-secret': this.configService.get<string>(
'graphql.adminSecret',
),
},
);

this.logger.debug('Update chapter likes');
this.logger.debug(updateResult);
}
}
}
6 changes: 6 additions & 0 deletions src/user/dto/like-chapter-request.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';

export class LikeChapterParam {
@ApiProperty()
chapterId: number;
}
17 changes: 16 additions & 1 deletion src/user/user.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,37 @@ import {
Delete,
UseGuards,
UseInterceptors,
Patch,
} from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';
import { UserService } from './user.service';
import { AuthGuard } from '../auth/auth.guard';
import { AuthUserInterceptor } from '../interceptors/auth-user-interceptor.service';
import { DeleteUserRequest } from './dto/delete-user-request.dto';
import { Roles } from '../auth/roles.decorator';
import { Role } from '../auth/role.enum';
import { RolesGuard } from '../auth/role.guard';
import { LikeChapterParam } from './dto/like-chapter-request.dto';

@Controller('user')
export class UserController {
constructor(private readonly userSvc: UserService) {}

@UseGuards(AuthGuard)
@UseGuards(AuthGuard, RolesGuard)
@ApiBearerAuth()
@Roles(Role.Admin)
@Delete()
@UseInterceptors(AuthUserInterceptor)
delete(@Query() data: DeleteUserRequest) {
return this.userSvc.delete(data);
}

@UseGuards(AuthGuard, RolesGuard)
@ApiBearerAuth()
@Roles(Role.User)
@Patch('like-chapter/:chapterId')
@UseInterceptors(AuthUserInterceptor)
likeChapter(@Query() data: LikeChapterParam) {
return this.userSvc.likeChapter(data);
}
}
4 changes: 3 additions & 1 deletion src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { GraphqlModule } from '../graphql/graphql.module';
import { RedisModule } from '../redis/redis.module';

@Module({
imports: [JwtModule],
imports: [JwtModule, GraphqlModule, RedisModule],
providers: [UserService],
controllers: [UserController],
})
Expand Down
71 changes: 65 additions & 6 deletions src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DeleteUserRequest } from './dto/delete-user-request.dto';
import { Authorizer } from '@authorizerdev/authorizer-js';
import { LikeChapterParam } from './dto/like-chapter-request.dto';
import { GraphqlService } from '../graphql/graphql.service';
import { ContextProvider } from '../providers/contex.provider';
import { RedisService } from '../redis/redis.service';

@Injectable()
export class UserService {
constructor(private config: ConfigService) {}
private readonly logger = new Logger(UserService.name);
constructor(
private configService: ConfigService,
private graphqlSvc: GraphqlService,
private redisClientService: RedisService,
) {}

async delete(data: DeleteUserRequest) {
const { email } = data;
Expand All @@ -19,15 +28,15 @@ export class UserService {
};

const headers = {
'x-authorizer-admin-secret': this.config.get<string>(
'x-authorizer-admin-secret': this.configService.get<string>(
'authorizer.adminSecret',
),
};

const authRef = new Authorizer({
redirectURL: this.config.get<string>('authorizer.redirectUrl'), // window.location.origin
authorizerURL: this.config.get<string>('authorizer.authorizerUrl'),
clientID: this.config.get<string>('authorizer.clientId'), // obtain your client id from authorizer dashboard
redirectURL: this.configService.get<string>('authorizer.redirectUrl'), // window.location.origin
authorizerURL: this.configService.get<string>('authorizer.authorizerUrl'),
clientID: this.configService.get<string>('authorizer.clientId'), // obtain your client id from authorizer dashboard
});

try {
Expand All @@ -47,4 +56,54 @@ export class UserService {
};
}
}

async likeChapter({ chapterId }: LikeChapterParam) {
const { token, userId } = ContextProvider.getAuthUser();

const result = await this.graphqlSvc.query(
this.configService.get<string>('graphql.endpoint'),
token,
`mutation UserLikeChapter ($chapter_id: Int!) {
insert_likes_one(object: {chapter_id:$chapter_id}) {
id
user_id
chapter_id
created_at
}
}`,
'UserLikeChapter',
{
chapter_id: chapterId,
},
);

// success
if (
result.data?.insert_likes_one &&
result.data?.insert_likes_one !== null
) {
this.logger.log(`User ${userId} like chapter ${chapterId}`);
// add to redis
this.redisClientService.client.sAdd(
[
this.configService.get<string>('app.name'),
this.configService.get<string>('app.env'),
'chapter-likes',
].join(':'),
chapterId.toString(),
);

// increase
this.redisClientService.client.incr(
[
this.configService.get<string>('app.name'),
this.configService.get<string>('app.env'),
'chapters',
chapterId.toString(),
'like',
].join(':'),
);
}
return result;
}
}

0 comments on commit 27dadaf

Please sign in to comment.