Skip to content

Commit

Permalink
Merge pull request #26 from beabee-communityrm/fix/1124-callout-filte…
Browse files Browse the repository at this point in the history
…r-cron

fix: callout filter in segment cronjob
  • Loading branch information
wpf500 authored Jul 24, 2024
2 parents f9f640c + f934081 commit e52ec17
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 72 deletions.
4 changes: 1 addition & 3 deletions apps/backend/src/api/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@ import { Contact } from "@beabee/core/models";
import config from "@beabee/core/config";

function currentUserChecker(action: { request: Request }): Contact | undefined {
return action.request.auth?.entity instanceof Contact
? action.request.auth.entity
: undefined;
return action.request.auth?.contact;
}

function authorizationChecker(
Expand Down
7 changes: 2 additions & 5 deletions apps/backend/src/api/decorators/TargetUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,8 @@ export function TargetUser() {
} else {
throw new NotFoundError();
}
} else if (
auth.entity instanceof Contact &&
(id === "me" || id === auth.entity.id)
) {
return auth.entity;
} else if (auth.contact && (id === "me" || id === auth.contact.id)) {
return auth.contact;
} else {
throw new UnauthorizedError();
}
Expand Down
5 changes: 2 additions & 3 deletions apps/backend/src/api/middlewares/AuthMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,23 +40,22 @@ async function getAuth(request: Request): Promise<AuthInfo | undefined> {
if (contact) {
return {
method: "api-key",
entity: contact,
contact,
// API can never acquire superadmin role
roles: contact.activeRoles.filter((r) => r !== "superadmin")
};
}
} else {
return {
method: "api-key",
entity: apiKey,
roles: apiKey.activeRoles
};
}
}
} else if (request.user) {
return {
method: "user",
entity: request.user,
contact: request.user,
roles: request.user.activeRoles
};
}
Expand Down
94 changes: 54 additions & 40 deletions apps/backend/src/api/transformers/BaseTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ import {
} from "@beabee/core/errors";
import { convertRulesToWhereClause } from "@api/utils/rules";

import { Contact } from "@beabee/core/models";

import { AuthInfo } from "@type/auth-info";
import { FilterHandlers } from "@type/filter-handlers";
import { AuthInfo, FetchRawResult, FilterHandlers } from "@type/index";

/**
* Base transformer for querying and converting models to DTOs
Expand Down Expand Up @@ -139,62 +136,79 @@ export abstract class BaseTransformer<
}

/**
* Fetch a list of items
* Fetch a list of items without converting them to DTOs
*
* @param auth The contact who is requesting the results
* @param query_ The query
* @param query
* @returns A list of items that match the query
*/
async fetch(
async fetchRaw(
auth: AuthInfo | undefined,
query_: Query
): Promise<PaginatedDto<GetDto>> {
): Promise<FetchRawResult<Model, Query>> {
const [query, filters, filterHandlers] = await this.preFetch(query_, auth);

const limit = query.limit || 50;
const offset = query.offset || 0;

let ruleGroup;
try {
const ruleGroup = query.rules && validateRuleGroup(filters, query.rules);
ruleGroup = query.rules && validateRuleGroup(filters, query.rules);
} catch (err) {
throw err instanceof InvalidRule
? new InvalidRuleError(err.rule, err.message)
: err;
}

const qb = createQueryBuilder(this.model, "item").offset(offset);
const qb = createQueryBuilder(this.model, "item").offset(offset);

if (limit !== -1) {
qb.limit(limit);
}
if (limit !== -1) {
qb.limit(limit);
}

if (ruleGroup) {
qb.where(
...convertRulesToWhereClause(
ruleGroup,
auth?.entity instanceof Contact ? auth.entity : undefined,
filterHandlers,
"item."
)
);
}
if (ruleGroup) {
qb.where(
...convertRulesToWhereClause(
ruleGroup,
auth?.contact,
filterHandlers,
"item."
)
);
}

if (query.sort) {
qb.orderBy(`item."${query.sort}"`, query.order || "ASC", "NULLS LAST");
}
if (query.sort) {
qb.orderBy(`item."${query.sort}"`, query.order || "ASC", "NULLS LAST");
}

this.modifyQueryBuilder(qb, "item.", query, auth);
this.modifyQueryBuilder(qb, "item.", query, auth);

const [items, total] = await qb.getManyAndCount();
const [items, total] = await qb.getManyAndCount();

await this.modifyItems(items, query, auth);
await this.modifyItems(items, query, auth);

return plainToInstance(PaginatedDto<GetDto>, {
total,
offset,
count: items.length,
items: items.map((item) => this.convert(item, query, auth))
});
} catch (err) {
throw err instanceof InvalidRule
? new InvalidRuleError(err.rule, err.message)
: err;
}
return { items, total, offset, query };
}

/**
* Fetch a list of items
*
* @param auth The contact who is requesting the results
* @param query_ The query
* @returns A list of items that match the query
*/
async fetch(
auth: AuthInfo | undefined,
query_: Query
): Promise<PaginatedDto<GetDto>> {
const { items, total, query, offset } = await this.fetchRaw(auth, query_);

return plainToInstance(PaginatedDto<GetDto>, {
total,
offset,
count: items.length,
items: items.map((item) => this.convert(item, query, auth))
});
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class CalloutResponseTransformer extends BaseCalloutResponseTransformer<
filters,
query2.rules,
responseUpdates,
auth?.entity instanceof Contact ? auth.entity : undefined,
auth?.contact,
filterHandlers,
(qb) => qb.returning(["id"])
);
Expand Down
20 changes: 7 additions & 13 deletions apps/backend/src/api/transformers/CalloutTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ import CalloutVariantTransformer from "@api/transformers/CalloutVariantTransform
import { groupBy } from "@api/utils";
import { mergeRules, statusFilterHandler } from "@api/utils/rules";

import {
Contact,
Callout,
CalloutResponse,
CalloutVariant
} from "@beabee/core/models";
import { Callout, CalloutResponse, CalloutVariant } from "@beabee/core/models";

import { AuthInfo } from "@type/auth-info";
import { FilterHandlers } from "@type/filter-handlers";
Expand Down Expand Up @@ -68,9 +63,11 @@ class CalloutTransformer extends BaseTransformer<
throw new BadRequestError("answeredBy only supports equal");
}

// Non-admins can only query for their own responses
if (
!auth?.roles.includes("admin") &&
args.value[0] !== auth?.entity.id
auth?.contact &&
!auth.roles.includes("admin") &&
args.value[0] !== auth.contact.id
) {
throw new UnauthorizedError();
}
Expand Down Expand Up @@ -264,16 +261,13 @@ class CalloutTransformer extends BaseTransformer<
}
}

if (
auth?.entity instanceof Contact &&
query.with?.includes(GetCalloutWith.HasAnswered)
) {
if (auth?.contact && query.with?.includes(GetCalloutWith.HasAnswered)) {
const answeredCallouts = await createQueryBuilder(CalloutResponse, "cr")
.select("cr.calloutId", "id")
.distinctOn(["cr.calloutId"])
.where("cr.calloutId IN (:...ids) AND cr.contactId = :id", {
ids: calloutIds,
id: auth.entity.id
id: auth.contact.id
})
.orderBy("cr.calloutId")
.getRawMany<{ id: string }>();
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/core/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function isValidNextUrl(url: string): boolean {
export function userToAuth(user: Express.User): AuthInfo {
return {
method: "user",
entity: user,
contact: user,
roles: user.activeRoles
};
}
Expand Down
12 changes: 10 additions & 2 deletions apps/backend/src/tools/process-segments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { In } from "typeorm";
import { getRepository } from "@beabee/core/database";
import { log as mainLogger } from "@beabee/core/logging";
import { runApp } from "@core/server";
import { getSegmentContacts } from "@core/utils/segments";

import EmailService from "@beabee/core/services/EmailService";
import NewsletterService from "@beabee/core/services/NewsletterService";
Expand All @@ -16,13 +15,22 @@ import {
SegmentOngoingEmail,
SegmentContact
} from "@beabee/core/models";
import ContactTransformer from "@api/transformers/ContactTransformer";
import { GetContactWith } from "@beabee/beabee-common";

const log = mainLogger.child({ app: "process-segments" });

async function processSegment(segment: Segment) {
log.info("Process segment " + segment.name);

const matchedContacts = await getSegmentContacts(segment);
const { items: matchedContacts } = await ContactTransformer.fetchRaw(
{ method: "internal", roles: ["admin"] },
{
limit: -1,
rules: segment.ruleGroup,
with: [GetContactWith.Profile, GetContactWith.Roles]
}
);

const segmentContacts = await getRepository(SegmentContact).find({
where: { segmentId: segment.id }
Expand Down
22 changes: 18 additions & 4 deletions apps/backend/src/type/auth-info.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { RoleType } from "@beabee/beabee-common";
import type { ApiKey, Contact } from "@beabee/core/models";
import type { Contact } from "@beabee/core/models";

export interface AuthInfo {
method: "user" | "api-key";
entity: Contact | ApiKey;
interface AuthInfoContact {
method: "user";
contact: Contact;
roles: RoleType[];
}

interface AuthInfoApiKey {
method: "api-key";
contact?: Contact;
roles: RoleType[];
}

interface AuthInfoInternal {
method: "internal";
contact?: undefined;
roles: RoleType[];
}

export type AuthInfo = AuthInfoContact | AuthInfoApiKey | AuthInfoInternal;
12 changes: 12 additions & 0 deletions apps/backend/src/type/fetch-raw-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PaginatedQuery } from "@beabee/beabee-common";
import { ObjectLiteral } from "typeorm";

export interface FetchRawResult<
Model extends ObjectLiteral,
Query extends PaginatedQuery
> {
items: Model[];
total: number;
query: Query;
offset: number;
}
1 change: 1 addition & 0 deletions apps/backend/src/type/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./auth-info.js";
export * from "./callout-map-schema.js";
export * from "./callout-response-view-schema.js";
export * from "./fetch-raw-result.js";
export * from "./filter-handlers.js";
export * from "./passport-local-done-callback.js";
export * from "./passport-local-strategy-options.js";
Expand Down

0 comments on commit e52ec17

Please sign in to comment.