Skip to content
This repository has been archived by the owner on Aug 11, 2024. It is now read-only.

Commit

Permalink
Merge branch 'master' into feat/917-response-validation
Browse files Browse the repository at this point in the history
  • Loading branch information
wpf500 committed Jan 19, 2024
2 parents 17e0c7d + ae49ff7 commit 9a0b924
Show file tree
Hide file tree
Showing 36 changed files with 359 additions and 254 deletions.
27 changes: 14 additions & 13 deletions src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import "reflect-metadata";
import { RoleType } from "@beabee/beabee-common";
import cookie from "cookie-parser";
import cors from "cors";
import express, { ErrorRequestHandler } from "express";
import express, { ErrorRequestHandler, Request } from "express";
import {
Action,
HttpError,
InternalServerError,
NotFoundError,
Expand All @@ -32,6 +31,8 @@ import { UploadController } from "./controllers/UploadController";

import { ValidateResponseInterceptor } from "./interceptors/ValidateResponseInterceptor";

import { AuthMiddleware } from "./middlewares/AuthMiddleware";

import {
log as mainLogger,
requestErrorLogger,
Expand All @@ -40,22 +41,21 @@ import {
import sessions from "@core/sessions";
import { initApp, startServer } from "@core/server";

import AuthService from "@core/services/AuthService";
import Contact from "@models/Contact";

import config from "@config";

async function currentUserChecker(action: Action) {
const apiKeyOrContact = await AuthService.check(action.request);
// API key isn't a user
return apiKeyOrContact === true ? undefined : apiKeyOrContact;
function currentUserChecker(action: { request: Request }): Contact | undefined {
return action.request.auth?.entity instanceof Contact
? action.request.auth.entity
: undefined;
}

async function authorizationChecker(action: Action, roles: RoleType[]) {
const apiKeyOrContact = await AuthService.check(action.request);
// API key has superadmin abilities
return apiKeyOrContact === true
? true
: roles.every((role) => apiKeyOrContact?.hasRole(role));
function authorizationChecker(
action: { request: Request },
roles: RoleType[]
): boolean {
return roles.every((r) => action.request.auth?.roles.includes(r));
}

const app = express();
Expand Down Expand Up @@ -91,6 +91,7 @@ initApp()
UploadController
],
interceptors: [ValidateResponseInterceptor],
middlewares: [AuthMiddleware],
currentUserChecker,
authorizationChecker,
validation: {
Expand Down
23 changes: 17 additions & 6 deletions src/api/controllers/ApiKeyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { generateApiKey } from "@core/utils/auth";

import ApiKey from "@models/ApiKey";
import Contact from "@models/Contact";
import UnauthorizedError from "@api/errors/UnauthorizedError";

import { CurrentAuth } from "@api/decorators/CurrentAuth";
import {
CreateApiKeyDto,
GetApiKeyDto,
Expand All @@ -28,30 +30,39 @@ import {
import { PaginatedDto } from "@api/dto/PaginatedDto";
import ApiKeyTransformer from "@api/transformers/ApiKeyTransformer";

import { AuthInfo } from "@type/auth-info";

@JsonController("/api-key")
@Authorized("admin")
export class ApiKeyController {
@Get("/")
async getApiKeys(
@CurrentUser({ required: true }) caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@QueryParams() query: ListApiKeysDto
): Promise<PaginatedDto<GetApiKeyDto>> {
return await ApiKeyTransformer.fetch(caller, query);
return await ApiKeyTransformer.fetch(auth, query);
}

@Get("/:id")
async getApiKey(
@CurrentUser({ required: true }) caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("id") id: string
): Promise<GetApiKeyDto | undefined> {
return await ApiKeyTransformer.fetchOneById(caller, id);
return await ApiKeyTransformer.fetchOneById(auth, id);
}

@Post("/")
async createApiKey(
@Body() data: CreateApiKeyDto,
@CurrentUser({ required: true }) creator: Contact
@CurrentAuth({ required: true }) auth: AuthInfo,
@CurrentUser({ required: true }) creator: Contact,
@Body() data: CreateApiKeyDto
): Promise<NewApiKeyDto> {
if (auth.method === "api-key") {
throw new UnauthorizedError({
message: "API key cannot create API keys"
});
}

const { id, secretHash, token } = generateApiKey();

await getRepository(ApiKey).save({
Expand Down
45 changes: 22 additions & 23 deletions src/api/controllers/CalloutController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
import { CreateCalloutTagDto, GetCalloutTagDto } from "@api/dto/CalloutTagDto";
import { PaginatedDto } from "@api/dto/PaginatedDto";

import { CurrentAuth } from "@api/decorators/CurrentAuth";
import PartialBody from "@api/decorators/PartialBody";
import DuplicateId from "@api/errors/DuplicateId";
import InvalidCalloutResponse from "@api/errors/InvalidCalloutResponse";
Expand All @@ -57,14 +58,16 @@ import CalloutResponse from "@models/CalloutResponse";
import CalloutResponseTag from "@models/CalloutResponseTag";
import CalloutTag from "@models/CalloutTag";

import { AuthInfo } from "@type/auth-info";

@JsonController("/callout")
export class CalloutController {
@Get("/")
async getCallouts(
@CurrentUser({ required: false }) caller: Contact | undefined,
@CurrentAuth() auth: AuthInfo | undefined,
@QueryParams() query: ListCalloutsDto
): Promise<PaginatedDto<GetCalloutDto>> {
return CalloutTransformer.fetch(caller, query);
return CalloutTransformer.fetch(auth, query);
}

@Authorized("admin")
Expand All @@ -82,11 +85,11 @@ export class CalloutController {

@Get("/:slug")
async getCallout(
@CurrentUser({ required: false }) caller: Contact | undefined,
@CurrentAuth() auth: AuthInfo | undefined,
@Param("slug") slug: string,
@QueryParams() query: GetCalloutOptsDto
): Promise<GetCalloutDto | undefined> {
return CalloutTransformer.fetchOneById(caller, slug, {
return CalloutTransformer.fetchOneById(auth, slug, {
...query,
showHiddenForAll: true
});
Expand All @@ -95,7 +98,7 @@ export class CalloutController {
@Authorized("admin")
@Patch("/:slug")
async updateCallout(
@CurrentUser({ required: true }) caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("slug") slug: string,
@PartialBody() data: CreateCalloutDto // Should be Partial<CreateCalloutData>
): Promise<GetCalloutDto | undefined> {
Expand All @@ -122,7 +125,7 @@ export class CalloutController {
data.formSchema as QueryDeepPartialEntity<CalloutFormSchema>
})
});
return await CalloutTransformer.fetchOneById(caller, newSlug);
return await CalloutTransformer.fetchOneById(auth, newSlug);
} catch (err) {
throw isDuplicateIndex(err, "slug") ? new DuplicateId(newSlug) : err;
}
Expand All @@ -141,26 +144,22 @@ export class CalloutController {

@Get("/:slug/responses")
async getCalloutResponses(
@CurrentUser() caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("slug") slug: string,
@QueryParams() query: ListCalloutResponsesDto
): Promise<PaginatedDto<GetCalloutResponseDto>> {
return await CalloutResponseTransformer.fetchForCallout(
caller,
slug,
query
);
return await CalloutResponseTransformer.fetchForCallout(auth, slug, query);
}

@Get("/:slug/responses.csv")
async exportCalloutResponses(
@CurrentUser() caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("slug") slug: string,
@QueryParams() query: GetExportQuery,
@Res() res: Response
): Promise<Response> {
const [exportName, exportData] = await CalloutResponseExporter.export(
caller,
auth,
slug,
query
);
Expand All @@ -170,12 +169,12 @@ export class CalloutController {

@Get("/:slug/responses/map")
async getCalloutResponsesMap(
@CurrentUser({ required: false }) caller: Contact | undefined,
@CurrentAuth() auth: AuthInfo | undefined,
@Param("slug") slug: string,
@QueryParams() query: ListCalloutResponsesDto
): Promise<PaginatedDto<GetCalloutResponseMapDto>> {
return await CalloutResponseMapTransformer.fetchForCallout(
caller,
auth,
slug,
query
);
Expand All @@ -184,7 +183,7 @@ export class CalloutController {
@Post("/:slug/responses")
@OnUndefined(204)
async createCalloutResponse(
@CurrentUser({ required: false }) caller: Contact | undefined,
@CurrentUser() caller: Contact | undefined,
@Param("slug") slug: string,
@Body() data: CreateCalloutResponseDto
): Promise<void> {
Expand Down Expand Up @@ -213,10 +212,10 @@ export class CalloutController {
@Authorized("admin")
@Get("/:slug/tags")
async getCalloutTags(
@CurrentUser() caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("slug") slug: string
): Promise<GetCalloutTagDto[]> {
const result = await CalloutTagTransformer.fetch(caller, {
const result = await CalloutTagTransformer.fetch(auth, {
rules: {
condition: "AND",
rules: [{ field: "calloutSlug", operator: "equal", value: [slug] }]
Expand Down Expand Up @@ -245,16 +244,16 @@ export class CalloutController {
@Authorized("admin")
@Get("/:slug/tags/:tag")
async getCalloutTag(
@CurrentUser() caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("tag") tagId: string
): Promise<GetCalloutTagDto | undefined> {
return CalloutTagTransformer.fetchOneById(caller, tagId);
return CalloutTagTransformer.fetchOneById(auth, tagId);
}

@Authorized("admin")
@Patch("/:slug/tags/:tag")
async updateCalloutTag(
@CurrentUser() caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Param("slug") slug: string,
@Param("tag") tagId: string,
@PartialBody() data: CreateCalloutTagDto // Partial<CreateCalloutTagData>
Expand All @@ -264,7 +263,7 @@ export class CalloutController {
data
);

return CalloutTagTransformer.fetchOneById(caller, tagId);
return CalloutTagTransformer.fetchOneById(auth, tagId);
}

@Authorized("admin")
Expand Down
17 changes: 10 additions & 7 deletions src/api/controllers/CalloutResponseCommentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {

import { getRepository } from "@core/database";

import { CurrentAuth } from "@api/decorators/CurrentAuth";
import PartialBody from "@api/decorators/PartialBody";
import {
CreateCalloutResponseCommentDto,
Expand All @@ -29,6 +30,8 @@ import CalloutResponseCommentTransformer from "@api/transformers/CalloutResponse
import CalloutResponseComment from "@models/CalloutResponseComment";
import Contact from "@models/Contact";

import { AuthInfo } from "@type/auth-info";

@JsonController("/callout-response-comments")
@Authorized("admin")
export class CalloutResponseCommentController {
Expand All @@ -40,37 +43,37 @@ export class CalloutResponseCommentController {
const comment: CalloutResponseComment = await getRepository(
CalloutResponseComment
).save({
contact,
text: data.text,
contact: contact,
response: { id: data.responseId }
});
return CalloutResponseCommentTransformer.convert(comment);
}

@Get("/")
async getCalloutResponseComments(
@CurrentUser({ required: true }) caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@QueryParams() query: ListCalloutResponseCommentsDto
): Promise<PaginatedDto<GetCalloutResponseCommentDto>> {
return await CalloutResponseCommentTransformer.fetch(caller, query);
return await CalloutResponseCommentTransformer.fetch(auth, query);
}

@Get("/:id")
async getCalloutResponseComment(
@CurrentUser({ required: true }) caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Params() { id }: UUIDParams
): Promise<GetCalloutResponseCommentDto | undefined> {
return await CalloutResponseCommentTransformer.fetchOneById(caller, id);
return await CalloutResponseCommentTransformer.fetchOneById(auth, id);
}

@Patch("/:id")
async updateCalloutResponseComment(
@CurrentUser({ required: true }) caller: Contact,
@CurrentAuth({ required: true }) auth: AuthInfo,
@Params() { id }: UUIDParams,
@PartialBody() data: CreateCalloutResponseCommentDto
): Promise<GetCalloutResponseCommentDto | undefined> {
await getRepository(CalloutResponseComment).update(id, data);
return await CalloutResponseCommentTransformer.fetchOneById(caller, id);
return await CalloutResponseCommentTransformer.fetchOneById(auth, id);
}

@OnUndefined(204)
Expand Down
Loading

0 comments on commit 9a0b924

Please sign in to comment.