Skip to content

Commit

Permalink
Merge pull request #80 from hwgilbert16/develop
Browse files Browse the repository at this point in the history
v1.10.10 Release
  • Loading branch information
hwgilbert16 authored Dec 24, 2023
2 parents 8a85f36 + d60e637 commit d3ae25d
Show file tree
Hide file tree
Showing 26 changed files with 966 additions and 119 deletions.
45 changes: 1 addition & 44 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,61 +6,18 @@ WORKDIR /usr/src/app
RUN apk add g++ make py3-pip

COPY package*.json .
RUN npm clean-install --production --silent --legacy-peer-deps
RUN npm install --omit=dev --legacy-peer-deps

COPY . .
RUN npm run generate
RUN npm run build

FROM node:lts-alpine3.18

ARG NODE_ENV
ARG DATABASE_PASSWORD
ARG DATABASE_URL
ARG JWT_SECRET
ARG HTTP_PORT

ARG SMTP_HOST
ARG SMTP_PORT
ARG SMTP_USERNAME
ARG SMTP_PASSWORD
ARG HOST
ARG SSL_KEY_BASE64
ARG SSL_CERT_BASE64
ARG SCHOLARSOME_RECAPTCHA_SITE
ARG SCHOLARSOME_RECAPTCHA_SECRET

WORKDIR /usr/src/app

COPY . .
COPY --from=builder /usr/src/app/dist ./dist
COPY --from=builder /usr/src/app/node_modules ./node_modules

RUN <<EOL
touch .env
echo "NODE_ENV=$NODE_ENV\n" >> .env
echo "DATABASE_PASSWORD=$DATABASE_PASSWORD\n" >> .env
echo "DATABASE_URL=$DATABASE_URL\n" >> .env
echo "JWT_SECRET=$JWT_SECRET\n" >> .env
echo "HTTP_PORT=$HTTP_PORT\n" >> .env
echo "S3_STORAGE_ENDPOINT=$S3_STORAGE_ENDPOINT\n" >> .env
echo "S3_STORAGE_ACCESS_KEY=$S3_STORAGE_ACCESS_KEY\n" >> .env
echo "S3_STORAGE_SECRET_KEY=$S3_STORAGE_SECRET_KEY\n" >> .env
echo "S3_STORAGE_REGION=$S3_STORAGE_REGION\n" >> .env
echo "S3_STORAGE_BUCKET=$S3_STORAGE_BUCKET\n" >> .env
echo "SMTP_HOST=$SMTP_HOST\n" >> .env
echo "SMTP_PORT=$SMTP_PORT\n" >> .env
echo "SMTP_USERNAME=$SMTP_USERNAME\n" >> .env
echo "SMTP_PASSWORD=$SMTP_PASSWORD\n" >> .env
echo "HOST=$HOST\n" >> .env
echo "SSL_KEY_BASE64=$SSL_KEY_BASE64\n" >> .env
echo "SSL_CERT_BASE64=$SSL_CERT_BASE64\n" >> .env
echo "SCHOLARSOME_RECAPTCHA_SITE=$SCHOLARSOME_RECAPTCHA_SITE\n" >> .env
echo "SCHOLARSOME_RECAPTCHA_SECRET=$SCHOLARSOME_RECAPTCHA_SECRET\n" >> .env
echo "REDIS_HOST=$REDIS_HOST" >> .env
echo "REDIS_PORT=$REDIS_PORT" >> .env
echo "REDIS_USERNAME=$REDIS_USERNAME" >> .env
echo "REDIS_PASSWORD=$REDIS_PASSWORD" >> .env
EOL

CMD [ "npm", "run", "serve:node" ]
8 changes: 4 additions & 4 deletions apps/api/src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ export class AuthController {
/**
* Sends a password reset for a given user
*
* @remarks Throttled to 5 requests per minute
* @remarks Throttled to 1 request per minute
* @returns Success response
*/
@Throttle(5, 600)
@Throttle(5, 60000)
@Get("reset/sendReset/:email")
async sendReset(
@Param() params: { email: string }
Expand Down Expand Up @@ -262,10 +262,10 @@ export class AuthController {
/**
* Registers a new user
*
* @remarks Throttled to 1 request per 15 minutes
* @remarks Throttled to 1 request per 3 minutes
* @returns Success response
*/
@Throttle(5, 900)
@Throttle(5, 180000)
@Post("register")
async register(
@Body() registerDto: RegisterDto,
Expand Down
28 changes: 28 additions & 0 deletions apps/api/src/app/sets/param/quizletExportParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { IsNotEmpty, IsString } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";

export class QuizletExportParams {
@ApiProperty({
description: "The ID of the set",
example: "19b86873-8e88-427b-839b-df02f1b8d5d8"
})
@IsString()
@IsNotEmpty()
setId: string;

@ApiProperty({
description: "The character(s) to separate each side of a card",
example: "["
})
@IsString()
@IsNotEmpty()
sideDiscriminator: string;

@ApiProperty({
description: "The character(s) to separate cards",
example: "["
})
@IsString()
@IsNotEmpty()
cardDiscriminator: string;
}
85 changes: 82 additions & 3 deletions apps/api/src/app/sets/sets.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
Param,
Patch,
Post,
Request,
Request, Response, StreamableFile,
UnauthorizedException,
UnsupportedMediaTypeException,
UploadedFile,
Expand All @@ -17,7 +17,7 @@ import {
import { AuthenticatedGuard } from "../auth/authenticated.guard";
import { SetsService } from "./sets.service";
import { UsersService } from "../users/users.service";
import { Request as ExpressRequest, Express } from "express";
import { Request as ExpressRequest, Response as ExpressResponse, Express } from "express";
import { ApiResponse, ApiResponseOptions } from "@scholarsome/shared";
import { Set } from "@prisma/client";
import { FileInterceptor } from "@nestjs/platform-express";
Expand All @@ -43,6 +43,8 @@ import { UserIdParam } from "../users/param/userId.param";
import { SetsSuccessResponse } from "./response/success/sets.success.response";
import { SetSuccessResponse } from "./response/success/set.success.response";
import { ErrorResponse } from "../shared/response/error.response";
import { Throttle } from "@nestjs/throttler";
// import { QuizletExportParams } from "./param/quizletExportParams";

@ApiTags("Sets")
@Controller("sets")
Expand Down Expand Up @@ -188,6 +190,83 @@ export class SetsController {
};
}

/**
* Converts a set to an Anki-compatible .apkg file
*
* @remarks Throttled to 1 request every 3 seconds
*/
@ApiOperation( {
summary: "Converts a set to an Anki-compatible .apkg file",
description: "Converts a Scholarsome set to an Anki-compatible .apkg file. Includes media (images, videos, etc) with the exported .apkg file."
})
@ApiUnauthorizedResponse({
description: "Invalid authentication to access the requested resource",
type: ErrorResponse
})
@Throttle(1, 3000)
@Get("export/anki/:setId")
async convertSetToAnkiApkg(@Param() params: SetIdParam, @Request() req: ExpressRequest, @Response({ passthrough: true }) res: ExpressResponse): Promise<StreamableFile> {
const set = await this.setsService.set({
id: params.setId
});
if (!set) throw new NotFoundException({ status: "fail", message: "Set not found" });

if (set.private) {
const userCookie = this.usersService.getUserInfo(req);

if (!userCookie) throw new UnauthorizedException({ status: "fail", message: "Invalid authentication to access the requested resource" });
if (set.authorId !== userCookie.id) throw new UnauthorizedException({ status: "fail", message: "Invalid authentication to access the requested resource" });
}

const apkg = await this.setsService.exportAsAnkiApkg(set);

res.set({
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename="${set.title + ".apkg"}`
});

return new StreamableFile(apkg);
}

// /**
// * Converts a set to a .txt file that can be imported into Quizlet
// *
// * @remarks Throttled to 1 request every 3 seconds
// */
// @ApiOperation( {
// summary: "Converts a set to a .txt that can be imported in Quizlet",
// description: "Converts a Scholarsome set to a .txt that can be imported in Quizlet. Media (images, videos, etc) will not be included in the exported .txt, as Quizlet does not provide an ability to import these materials."
// })
// @ApiUnauthorizedResponse({
// description: "Invalid authentication to access the requested resource",
// type: ErrorResponse
// })
// @Throttle(1, 3000)
// @Get("export/quizlet/:setId/:sideDiscriminator/:cardDiscriminator")
// async convertSetToQuizletTxt(@Param() params: QuizletExportParams, @Request() req: ExpressRequest, @Response({ passthrough: true }) res: ExpressResponse): Promise<StreamableFile> {
// const set = await this.setsService.set({
// id: params.setId
// });
// if (!set) throw new NotFoundException({ status: "fail", message: "Set not found" });
//
// if (set.private) {
// const userCookie = this.usersService.getUserInfo(req);
//
// if (!userCookie) throw new UnauthorizedException({ status: "fail", message: "Invalid authentication to access the requested resource" });
// if (set.authorId !== userCookie.id) throw new UnauthorizedException({ status: "fail", message: "Invalid authentication to access the requested resource" });
// }
//
// const txt = this.setsService.exportAsQuizletTxt(set, params.sideDiscriminator, params.cardDiscriminator);
// if (!txt) throw new BadRequestException("At least one card in the set contains side or card discriminator characters. The set must not contain the characters being used to format the exported set.");
//
// res.set({
// "Content-Type": "application/octet-stream",
// "Content-Disposition": `attachment; filename="${set.title + ".txt"}`
// });
//
// return new StreamableFile(txt);
// }

/**
* Creates a set from an Anki .apkg file
*
Expand All @@ -213,7 +292,7 @@ export class SetsController {
@UseGuards(AuthenticatedGuard)
@UseInterceptors(FileInterceptor("file"))
@Post("apkg")
async createSetFromApkg(@Body() body: CreateSetFromApkgDto, @Request() req: ExpressRequest, @UploadedFile() file: Express.Multer.File): Promise<ApiResponse<Set>> {
async createSetFromAnkiApkg(@Body() body: CreateSetFromApkgDto, @Request() req: ExpressRequest, @UploadedFile() file: Express.Multer.File): Promise<ApiResponse<Set>> {
const user = this.usersService.getUserInfo(req);
if (!user) throw new UnauthorizedException({ status: "fail", message: "Invalid authentication to access the requested resource" });

Expand Down
Loading

0 comments on commit d3ae25d

Please sign in to comment.