@bash-app/bash-common 30.230.0 → 30.232.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/userReportTypes.test.d.ts +2 -0
- package/dist/__tests__/userReportTypes.test.d.ts.map +1 -0
- package/dist/__tests__/userReportTypes.test.js +81 -0
- package/dist/__tests__/userReportTypes.test.js.map +1 -0
- package/dist/bashFeedTypes.d.ts +10 -0
- package/dist/bashFeedTypes.d.ts.map +1 -1
- package/dist/definitions.d.ts +10 -0
- package/dist/definitions.d.ts.map +1 -1
- package/dist/definitions.js +8 -0
- package/dist/definitions.js.map +1 -1
- package/dist/extendedSchemas.d.ts +18 -1
- package/dist/extendedSchemas.d.ts.map +1 -1
- package/dist/extendedSchemas.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/userReportTypes.d.ts +117 -0
- package/dist/userReportTypes.d.ts.map +1 -0
- package/dist/userReportTypes.js +99 -0
- package/dist/userReportTypes.js.map +1 -0
- package/dist/utils/__tests__/getBackendHost.test.js +21 -1
- package/dist/utils/__tests__/getBackendHost.test.js.map +1 -1
- package/dist/utils/__tests__/getFrontendHost.native.test.d.ts +14 -0
- package/dist/utils/__tests__/getFrontendHost.native.test.d.ts.map +1 -0
- package/dist/utils/__tests__/getFrontendHost.native.test.js +51 -0
- package/dist/utils/__tests__/getFrontendHost.native.test.js.map +1 -0
- package/dist/utils/slugUtils.d.ts +4 -0
- package/dist/utils/slugUtils.d.ts.map +1 -1
- package/dist/utils/slugUtils.js +9 -1
- package/dist/utils/slugUtils.js.map +1 -1
- package/dist/utils/urlUtils.d.ts.map +1 -1
- package/dist/utils/urlUtils.js +7 -0
- package/dist/utils/urlUtils.js.map +1 -1
- package/package.json +3 -1
- package/prisma/schema.prisma +23 -0
- package/src/__tests__/userReportTypes.test.ts +103 -0
- package/src/bashFeedTypes.ts +13 -1
- package/src/definitions.ts +13 -0
- package/src/extendedSchemas.ts +23 -0
- package/src/index.ts +1 -0
- package/src/userReportTypes.ts +252 -0
- package/src/utils/__tests__/getBackendHost.test.ts +28 -1
- package/src/utils/__tests__/getFrontendHost.native.test.ts +75 -0
- package/src/utils/slugUtils.ts +24 -1
- package/src/utils/urlUtils.ts +9 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bash-app/bash-common",
|
|
3
|
-
"version": "30.
|
|
3
|
+
"version": "30.232.0",
|
|
4
4
|
"description": "Common data and scripts to use on the frontend and backend",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "jest --config jest.config.cjs",
|
|
10
|
+
"knip": "knip --config knip.config.ts",
|
|
10
11
|
"build": "npm run generate && npm run tsc",
|
|
11
12
|
"generate": "prisma generate",
|
|
12
13
|
"db": "prisma generate && prisma db push",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@types/react": "^18.3.2",
|
|
49
50
|
"@types/shelljs": "^0.10.0",
|
|
50
51
|
"jest": "^29.7.0",
|
|
52
|
+
"knip": "^6.11.0",
|
|
51
53
|
"npm": "^11.11.0",
|
|
52
54
|
"shelljs": "^0.10.0",
|
|
53
55
|
"stripe": "^20.1.2",
|
package/prisma/schema.prisma
CHANGED
|
@@ -2539,6 +2539,7 @@ model User {
|
|
|
2539
2539
|
reportsReceived UserReport[] @relation("ReportsReceived")
|
|
2540
2540
|
reportsMade UserReport[] @relation("ReportsMade")
|
|
2541
2541
|
reportsReviewed UserReport[] @relation("ReportsReviewed")
|
|
2542
|
+
reportEvidenceUploaded UserReportEvidence[] @relation("UserReportEvidenceUploader")
|
|
2542
2543
|
userStats UserStats?
|
|
2543
2544
|
userSubscription UserSubscription?
|
|
2544
2545
|
vendorBid VendorBid[]
|
|
@@ -4426,6 +4427,7 @@ model UserReport {
|
|
|
4426
4427
|
reviewedAt DateTime?
|
|
4427
4428
|
reviewNotes String?
|
|
4428
4429
|
demerits Demerit[]
|
|
4430
|
+
evidence UserReportEvidence[]
|
|
4429
4431
|
bashEvent BashEvent? @relation(fields: [bashEventId], references: [id])
|
|
4430
4432
|
reported User @relation("ReportsReceived", fields: [reportedId], references: [id], onDelete: Cascade)
|
|
4431
4433
|
reporter User @relation("ReportsMade", fields: [reporterId], references: [id], onDelete: Cascade)
|
|
@@ -4437,6 +4439,22 @@ model UserReport {
|
|
|
4437
4439
|
@@index([status])
|
|
4438
4440
|
}
|
|
4439
4441
|
|
|
4442
|
+
model UserReportEvidence {
|
|
4443
|
+
id String @id @default(cuid())
|
|
4444
|
+
reportId String
|
|
4445
|
+
url String
|
|
4446
|
+
s3Key String
|
|
4447
|
+
mimetype String
|
|
4448
|
+
sizeBytes Int
|
|
4449
|
+
uploadedById String
|
|
4450
|
+
createdAt DateTime @default(now())
|
|
4451
|
+
report UserReport @relation(fields: [reportId], references: [id], onDelete: Cascade)
|
|
4452
|
+
uploadedBy User @relation("UserReportEvidenceUploader", fields: [uploadedById], references: [id], onDelete: Cascade)
|
|
4453
|
+
|
|
4454
|
+
@@index([reportId])
|
|
4455
|
+
@@index([uploadedById])
|
|
4456
|
+
}
|
|
4457
|
+
|
|
4440
4458
|
model Demerit {
|
|
4441
4459
|
id String @id @default(cuid())
|
|
4442
4460
|
userId String
|
|
@@ -5485,6 +5503,7 @@ enum AudioVisualSupportSubType {
|
|
|
5485
5503
|
|
|
5486
5504
|
enum PromotionAndMarketingSubType {
|
|
5487
5505
|
Influencer
|
|
5506
|
+
ContentCreator
|
|
5488
5507
|
Promoter
|
|
5489
5508
|
CelebrityAppearance
|
|
5490
5509
|
}
|
|
@@ -7505,6 +7524,10 @@ enum AuditAction {
|
|
|
7505
7524
|
USER_SUSPENDED
|
|
7506
7525
|
USER_UNSUSPENDED
|
|
7507
7526
|
USER_DELETED
|
|
7527
|
+
USER_DEACTIVATED
|
|
7528
|
+
USER_DEMERITED
|
|
7529
|
+
USER_REPORTED
|
|
7530
|
+
USER_REPORT_IGNORED
|
|
7508
7531
|
USER_DATA_EXPORTED
|
|
7509
7532
|
PROFILE_UPDATED
|
|
7510
7533
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALL_USER_REPORT_REASON_CODES,
|
|
3
|
+
REPORT_REASON_FUNNEL,
|
|
4
|
+
USER_REPORT_DETAILS_MAX_LENGTH,
|
|
5
|
+
USER_REPORT_EVIDENCE_MAX_BYTES,
|
|
6
|
+
USER_REPORT_EVIDENCE_MAX_FILES,
|
|
7
|
+
getUserReportReasonLabel,
|
|
8
|
+
getUserReportReasonPath,
|
|
9
|
+
isUserReportReasonCode,
|
|
10
|
+
} from "../userReportTypes.js";
|
|
11
|
+
|
|
12
|
+
describe("userReportTypes", () => {
|
|
13
|
+
describe("REPORT_REASON_FUNNEL", () => {
|
|
14
|
+
it("contains the six top-level categories", () => {
|
|
15
|
+
const codes = REPORT_REASON_FUNNEL.map((c) => c.code);
|
|
16
|
+
expect(codes).toEqual([
|
|
17
|
+
"HARASSMENT",
|
|
18
|
+
"IMPERSONATION",
|
|
19
|
+
"SPAM",
|
|
20
|
+
"UNSAFE",
|
|
21
|
+
"INAPPROPRIATE",
|
|
22
|
+
"OTHER",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("each category has at least one sub-reason", () => {
|
|
27
|
+
for (const cat of REPORT_REASON_FUNNEL) {
|
|
28
|
+
expect(cat.reasons.length).toBeGreaterThan(0);
|
|
29
|
+
for (const r of cat.reasons) {
|
|
30
|
+
expect(r.code).toBeTruthy();
|
|
31
|
+
expect(r.label).toBeTruthy();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("sub-reason codes are unique across the funnel", () => {
|
|
37
|
+
const seen = new Set<string>();
|
|
38
|
+
for (const cat of REPORT_REASON_FUNNEL) {
|
|
39
|
+
for (const r of cat.reasons) {
|
|
40
|
+
expect(seen.has(r.code)).toBe(false);
|
|
41
|
+
seen.add(r.code);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("ALL_USER_REPORT_REASON_CODES mirrors the funnel", () => {
|
|
47
|
+
const flat = REPORT_REASON_FUNNEL.flatMap((c) => c.reasons.map((r) => r.code));
|
|
48
|
+
expect([...ALL_USER_REPORT_REASON_CODES]).toEqual(flat);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("isUserReportReasonCode", () => {
|
|
53
|
+
it("accepts any code in the funnel", () => {
|
|
54
|
+
for (const code of ALL_USER_REPORT_REASON_CODES) {
|
|
55
|
+
expect(isUserReportReasonCode(code)).toBe(true);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
["unknown_code"],
|
|
61
|
+
[""],
|
|
62
|
+
["spam_promotion"],
|
|
63
|
+
["HARASSMENT"],
|
|
64
|
+
])("rejects unknown / wrong-shape value: %s", (val) => {
|
|
65
|
+
expect(isUserReportReasonCode(val)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("rejects non-string values", () => {
|
|
69
|
+
expect(isUserReportReasonCode(undefined)).toBe(false);
|
|
70
|
+
expect(isUserReportReasonCode(null)).toBe(false);
|
|
71
|
+
expect(isUserReportReasonCode(42)).toBe(false);
|
|
72
|
+
expect(isUserReportReasonCode({})).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("getUserReportReasonLabel / getUserReportReasonPath", () => {
|
|
77
|
+
it("returns the human label for a known code", () => {
|
|
78
|
+
expect(getUserReportReasonLabel("SPAM_SCAM")).toBe("Scam, fraud, or phishing");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("returns the original code when unknown", () => {
|
|
82
|
+
expect(getUserReportReasonLabel("nope")).toBe("nope");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("formats path as 'Category › Sub-reason'", () => {
|
|
86
|
+
expect(getUserReportReasonPath("HARASSMENT_BULLYING")).toBe(
|
|
87
|
+
"Harassment or bullying › Bullying or repeated targeting"
|
|
88
|
+
);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns the original code for path lookup of unknown", () => {
|
|
92
|
+
expect(getUserReportReasonPath("missing")).toBe("missing");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("limits constants", () => {
|
|
97
|
+
it("are conservative enough for a friendly UI but not absurd", () => {
|
|
98
|
+
expect(USER_REPORT_EVIDENCE_MAX_FILES).toBe(5);
|
|
99
|
+
expect(USER_REPORT_EVIDENCE_MAX_BYTES).toBe(25 * 1024 * 1024);
|
|
100
|
+
expect(USER_REPORT_DETAILS_MAX_LENGTH).toBe(4000);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
package/src/bashFeedTypes.ts
CHANGED
|
@@ -27,7 +27,16 @@ export interface BashFeedPostBashSummary {
|
|
|
27
27
|
allowed?: string | null;
|
|
28
28
|
notAllowed?: string | null;
|
|
29
29
|
averageRating?: number | null;
|
|
30
|
-
_count?: {
|
|
30
|
+
_count?: {
|
|
31
|
+
tickets: number;
|
|
32
|
+
interests?: number;
|
|
33
|
+
/** Tickets with `checkedInAt` set (see feed attach util). */
|
|
34
|
+
checkedIn?: number;
|
|
35
|
+
/** `PublicBashRsvp` rows with status Going. */
|
|
36
|
+
rsvpGoing?: number;
|
|
37
|
+
/** `PublicBashRsvp` rows with status Maybe. */
|
|
38
|
+
rsvpMaybe?: number;
|
|
39
|
+
};
|
|
31
40
|
activeOffers?: Array<{
|
|
32
41
|
id: string;
|
|
33
42
|
offerType: string;
|
|
@@ -146,6 +155,9 @@ export interface BashFeedPostBashSummaryWire {
|
|
|
146
155
|
tickets: number;
|
|
147
156
|
/** IdeaInterest rows (Prisma relation name `interests`) */
|
|
148
157
|
interests?: number;
|
|
158
|
+
checkedIn?: number;
|
|
159
|
+
rsvpGoing?: number;
|
|
160
|
+
rsvpMaybe?: number;
|
|
149
161
|
};
|
|
150
162
|
}
|
|
151
163
|
|
package/src/definitions.ts
CHANGED
|
@@ -297,6 +297,17 @@ export const STRIPE_IDENTITY_RETURN_URL = `/verify-id` as const;
|
|
|
297
297
|
export const MY_SERVICES_URL = "/my-services" as const;
|
|
298
298
|
export const BASH_DETAIL_URL = `/bash` as const;
|
|
299
299
|
export const BASH_DETAIL_URL_PATTERN = `/bash/$bashEventIdSlug` as const;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Bash event wizard indices for the "How" major step. Must match bash-app
|
|
303
|
+
* `bashEventWizardHowSubsteps.ts` / `BashEventWizard.tsx`.
|
|
304
|
+
*/
|
|
305
|
+
export const BASH_EVENT_WIZARD_HOW_MAJOR_STEP = 7;
|
|
306
|
+
/** Payout / Stripe Connect substep within "How" (app `BASH_EVENT_WIZARD_HOW_SUBSTEP.PAYOUT`). */
|
|
307
|
+
export const BASH_EVENT_WIZARD_HOW_SUBSTEP_PAYOUT = 8;
|
|
308
|
+
|
|
309
|
+
export const BASH_EVENT_WIZARD_PATH_PREFIX = "/bash-event-wizard" as const;
|
|
310
|
+
|
|
300
311
|
export const SERVICE_PAGE_URL = `/service-page` as const;
|
|
301
312
|
export const LOGIN_URL = `/login` as const;
|
|
302
313
|
export const TICKET_DETAILS = `/ticket-details` as const;
|
|
@@ -616,6 +627,8 @@ export interface ApiResult<T, P extends ErrorDataType = ErrorDataType> {
|
|
|
616
627
|
/** e.g. safe path to navigate after password reset */
|
|
617
628
|
returnTo?: string;
|
|
618
629
|
error?: string;
|
|
630
|
+
/** Machine-readable error code for clients (e.g. duplicate ticket transfer). */
|
|
631
|
+
code?: string;
|
|
619
632
|
errorType?: ApiErrorType;
|
|
620
633
|
errorData?: P;
|
|
621
634
|
rawResponse?: Response;
|
package/src/extendedSchemas.ts
CHANGED
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
TicketMetadata,
|
|
69
69
|
TicketTier,
|
|
70
70
|
TicketTransfer,
|
|
71
|
+
TicketTransferInvite,
|
|
71
72
|
User,
|
|
72
73
|
UserPreferences,
|
|
73
74
|
UserStats,
|
|
@@ -866,6 +867,17 @@ export interface ServiceBookingExt extends ServiceBooking {
|
|
|
866
867
|
|
|
867
868
|
export type ServiceBookingPublicExt = Omit<ServiceBookingExt, "checkout">;
|
|
868
869
|
|
|
870
|
+
/**
|
|
871
|
+
* Aggregated booking activity attached to public/owner service list payloads.
|
|
872
|
+
* `confirmedBookings` is the "completed bookings" signal used to rank services
|
|
873
|
+
* by Popularity on the home page.
|
|
874
|
+
*/
|
|
875
|
+
export interface ServiceBookingStats {
|
|
876
|
+
totalBookings: number;
|
|
877
|
+
confirmedBookings: number;
|
|
878
|
+
lastCompletedBooking: Date | string | null;
|
|
879
|
+
}
|
|
880
|
+
|
|
869
881
|
export interface ServiceExt extends Service {
|
|
870
882
|
owner?: PublicUser | null;
|
|
871
883
|
creator?: PublicUser | null;
|
|
@@ -903,6 +915,9 @@ export interface ServiceExt extends Service {
|
|
|
903
915
|
/** Some list endpoints historically used this key; prefer `bookings` */
|
|
904
916
|
serviceBookings?: ServiceBookingExt[];
|
|
905
917
|
|
|
918
|
+
/** Attached by list endpoints (public/owner) for popularity sort and recency badges. */
|
|
919
|
+
bookingStats?: ServiceBookingStats;
|
|
920
|
+
|
|
906
921
|
// bookedCheckouts: ServiceBookingCheckoutExt[]; //not necessary to include
|
|
907
922
|
}
|
|
908
923
|
|
|
@@ -1223,6 +1238,14 @@ export interface TicketExt extends Ticket {
|
|
|
1223
1238
|
checkout?: Checkout;
|
|
1224
1239
|
transfers?: TicketTransfer[];
|
|
1225
1240
|
metadata?: TicketMetadata[];
|
|
1241
|
+
/**
|
|
1242
|
+
* Included on authenticated GET /event/:id for the viewer's tickets when a
|
|
1243
|
+
* peer transfer invite row exists (see transfer-withdraw / check-in guards).
|
|
1244
|
+
*/
|
|
1245
|
+
transferInvite?: Pick<
|
|
1246
|
+
TicketTransferInvite,
|
|
1247
|
+
"status" | "expiresAt" | "token" | "toEmail" | "toUserId"
|
|
1248
|
+
> | null;
|
|
1226
1249
|
}
|
|
1227
1250
|
|
|
1228
1251
|
export interface CheckoutExt extends Checkout {
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from "./partnerStoreTypes.js";
|
|
|
8
8
|
export * from "./bashFeedTypes.js";
|
|
9
9
|
export * from "./membershipDefinitions.js";
|
|
10
10
|
export * from "./aiApproval.js";
|
|
11
|
+
export * from "./userReportTypes.js";
|
|
11
12
|
|
|
12
13
|
// Re-export ALL Prisma enums as values (usable as runtime constants, e.g. ServiceTypes.Entertainment)
|
|
13
14
|
// Excludes enums already re-exported by definitions.ts and membershipDefinitions.ts:
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the user-report flow (public profile → admin dashboard).
|
|
3
|
+
* Reporting is a real-stakes flow, so all user-facing copy here is plain and specific.
|
|
4
|
+
*
|
|
5
|
+
* The selected sub-reason code is stored verbatim in `UserReport.reason`.
|
|
6
|
+
* Free text is stored in `UserReport.details`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Top-level funnel category. */
|
|
10
|
+
export type UserReportCategoryCode =
|
|
11
|
+
| "HARASSMENT"
|
|
12
|
+
| "IMPERSONATION"
|
|
13
|
+
| "SPAM"
|
|
14
|
+
| "UNSAFE"
|
|
15
|
+
| "INAPPROPRIATE"
|
|
16
|
+
| "OTHER";
|
|
17
|
+
|
|
18
|
+
/** Specific sub-reason. Persisted as `UserReport.reason`. */
|
|
19
|
+
export type UserReportReasonCode =
|
|
20
|
+
// HARASSMENT
|
|
21
|
+
| "HARASSMENT_BULLYING"
|
|
22
|
+
| "HARASSMENT_THREATS"
|
|
23
|
+
| "HARASSMENT_HATE_SPEECH"
|
|
24
|
+
| "HARASSMENT_TARGETED"
|
|
25
|
+
// IMPERSONATION
|
|
26
|
+
| "IMPERSONATION_ME"
|
|
27
|
+
| "IMPERSONATION_SOMEONE_ELSE"
|
|
28
|
+
| "IMPERSONATION_FAKE_BUSINESS"
|
|
29
|
+
// SPAM
|
|
30
|
+
| "SPAM_PROMOTION"
|
|
31
|
+
| "SPAM_SCAM"
|
|
32
|
+
| "SPAM_FAKE_ENGAGEMENT"
|
|
33
|
+
// UNSAFE
|
|
34
|
+
| "UNSAFE_VIOLENT"
|
|
35
|
+
| "UNSAFE_SELF_HARM"
|
|
36
|
+
| "UNSAFE_MINORS"
|
|
37
|
+
| "UNSAFE_ILLEGAL_GOODS"
|
|
38
|
+
// INAPPROPRIATE
|
|
39
|
+
| "INAPPROPRIATE_NUDITY_SEXUAL"
|
|
40
|
+
| "INAPPROPRIATE_GRAPHIC_VIOLENCE"
|
|
41
|
+
| "INAPPROPRIATE_OFFENSIVE"
|
|
42
|
+
// OTHER
|
|
43
|
+
| "OTHER";
|
|
44
|
+
|
|
45
|
+
export interface UserReportReasonOption {
|
|
46
|
+
code: UserReportReasonCode;
|
|
47
|
+
label: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UserReportCategoryOption {
|
|
51
|
+
code: UserReportCategoryCode;
|
|
52
|
+
label: string;
|
|
53
|
+
description: string;
|
|
54
|
+
reasons: UserReportReasonOption[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Funnel definition shared between the report modal (UI) and the API
|
|
59
|
+
* (validation that the chosen `reason` is a known code).
|
|
60
|
+
*/
|
|
61
|
+
export const REPORT_REASON_FUNNEL: readonly UserReportCategoryOption[] = [
|
|
62
|
+
{
|
|
63
|
+
code: "HARASSMENT",
|
|
64
|
+
label: "Harassment or bullying",
|
|
65
|
+
description: "Behavior that targets, intimidates, or threatens someone.",
|
|
66
|
+
reasons: [
|
|
67
|
+
{ code: "HARASSMENT_BULLYING", label: "Bullying or repeated targeting" },
|
|
68
|
+
{ code: "HARASSMENT_THREATS", label: "Threats of violence or harm" },
|
|
69
|
+
{ code: "HARASSMENT_HATE_SPEECH", label: "Hate speech or discrimination" },
|
|
70
|
+
{ code: "HARASSMENT_TARGETED", label: "Targeting me or someone I know" },
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
code: "IMPERSONATION",
|
|
75
|
+
label: "Impersonation",
|
|
76
|
+
description: "Pretending to be a person or business they aren't.",
|
|
77
|
+
reasons: [
|
|
78
|
+
{ code: "IMPERSONATION_ME", label: "Pretending to be me" },
|
|
79
|
+
{ code: "IMPERSONATION_SOMEONE_ELSE", label: "Pretending to be someone else" },
|
|
80
|
+
{ code: "IMPERSONATION_FAKE_BUSINESS", label: "Pretending to be a business or venue" },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: "SPAM",
|
|
85
|
+
label: "Spam or scam",
|
|
86
|
+
description: "Unsolicited promotion, scams, or fake engagement.",
|
|
87
|
+
reasons: [
|
|
88
|
+
{ code: "SPAM_PROMOTION", label: "Unsolicited promotion" },
|
|
89
|
+
{ code: "SPAM_SCAM", label: "Scam, fraud, or phishing" },
|
|
90
|
+
{ code: "SPAM_FAKE_ENGAGEMENT", label: "Fake followers, ratings, or reviews" },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
code: "UNSAFE",
|
|
95
|
+
label: "Unsafe or threatening behavior",
|
|
96
|
+
description: "Activity that puts people at risk.",
|
|
97
|
+
reasons: [
|
|
98
|
+
{ code: "UNSAFE_VIOLENT", label: "Violent behavior" },
|
|
99
|
+
{ code: "UNSAFE_SELF_HARM", label: "Self-harm or suicide" },
|
|
100
|
+
{ code: "UNSAFE_MINORS", label: "Endangering a minor" },
|
|
101
|
+
{ code: "UNSAFE_ILLEGAL_GOODS", label: "Illegal goods or services" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
code: "INAPPROPRIATE",
|
|
106
|
+
label: "Inappropriate content",
|
|
107
|
+
description: "Content that doesn't belong on Bash.",
|
|
108
|
+
reasons: [
|
|
109
|
+
{ code: "INAPPROPRIATE_NUDITY_SEXUAL", label: "Nudity or sexual content" },
|
|
110
|
+
{ code: "INAPPROPRIATE_GRAPHIC_VIOLENCE", label: "Graphic violence" },
|
|
111
|
+
{ code: "INAPPROPRIATE_OFFENSIVE", label: "Offensive or disturbing content" },
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
code: "OTHER",
|
|
116
|
+
label: "Something else",
|
|
117
|
+
description: "Tell us in your own words.",
|
|
118
|
+
reasons: [{ code: "OTHER", label: "Other (describe below)" }],
|
|
119
|
+
},
|
|
120
|
+
] as const;
|
|
121
|
+
|
|
122
|
+
/** All valid reason codes (built from the funnel — no drift). */
|
|
123
|
+
export const ALL_USER_REPORT_REASON_CODES: readonly UserReportReasonCode[] =
|
|
124
|
+
REPORT_REASON_FUNNEL.flatMap((c) => c.reasons.map((r) => r.code));
|
|
125
|
+
|
|
126
|
+
export const isUserReportReasonCode = (
|
|
127
|
+
value: unknown
|
|
128
|
+
): value is UserReportReasonCode =>
|
|
129
|
+
typeof value === "string" &&
|
|
130
|
+
(ALL_USER_REPORT_REASON_CODES as readonly string[]).includes(value);
|
|
131
|
+
|
|
132
|
+
/** Look up the human-readable label for a reason code. */
|
|
133
|
+
export function getUserReportReasonLabel(code: string): string {
|
|
134
|
+
for (const cat of REPORT_REASON_FUNNEL) {
|
|
135
|
+
const match = cat.reasons.find((r) => r.code === code);
|
|
136
|
+
if (match) return match.label;
|
|
137
|
+
}
|
|
138
|
+
return code;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Look up "Category › Reason" path for a stored reason code. */
|
|
142
|
+
export function getUserReportReasonPath(code: string): string {
|
|
143
|
+
for (const cat of REPORT_REASON_FUNNEL) {
|
|
144
|
+
const match = cat.reasons.find((r) => r.code === code);
|
|
145
|
+
if (match) return `${cat.label} › ${match.label}`;
|
|
146
|
+
}
|
|
147
|
+
return code;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Constants used by the evidence upload route. */
|
|
151
|
+
export const USER_REPORT_EVIDENCE_MAX_FILES = 5;
|
|
152
|
+
export const USER_REPORT_EVIDENCE_MAX_BYTES = 25 * 1024 * 1024;
|
|
153
|
+
|
|
154
|
+
/** Max free-text length for `UserReport.details`. */
|
|
155
|
+
export const USER_REPORT_DETAILS_MAX_LENGTH = 4000;
|
|
156
|
+
|
|
157
|
+
// ─── DTOs ────────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
export interface CreateUserReportRequest {
|
|
160
|
+
reportedId: string;
|
|
161
|
+
reason: UserReportReasonCode;
|
|
162
|
+
details?: string;
|
|
163
|
+
bashEventId?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface UserReportEvidenceDto {
|
|
167
|
+
id: string;
|
|
168
|
+
url: string;
|
|
169
|
+
mimetype: string;
|
|
170
|
+
sizeBytes: number;
|
|
171
|
+
createdAt: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface UserReportUserSummary {
|
|
175
|
+
id: string;
|
|
176
|
+
email: string | null;
|
|
177
|
+
username: string | null;
|
|
178
|
+
givenName: string | null;
|
|
179
|
+
familyName: string | null;
|
|
180
|
+
image: string | null;
|
|
181
|
+
uploadedImage: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface UserReportListItem {
|
|
185
|
+
id: string;
|
|
186
|
+
reporterId: string;
|
|
187
|
+
reportedId: string;
|
|
188
|
+
reason: UserReportReasonCode | string;
|
|
189
|
+
details: string;
|
|
190
|
+
status: string;
|
|
191
|
+
createdAt: string;
|
|
192
|
+
reviewerId: string | null;
|
|
193
|
+
reviewedAt: string | null;
|
|
194
|
+
reviewNotes: string | null;
|
|
195
|
+
evidenceCount: number;
|
|
196
|
+
reporter: UserReportUserSummary;
|
|
197
|
+
reported: UserReportUserSummary;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export interface UserReportDetail extends Omit<UserReportListItem, "evidenceCount"> {
|
|
201
|
+
evidence: UserReportEvidenceDto[];
|
|
202
|
+
/** Other pending reports against the same reported user. */
|
|
203
|
+
otherPendingReportsAgainstReported: number;
|
|
204
|
+
/** Total active demerits already on the reported user. */
|
|
205
|
+
reportedActiveDemerits: number;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Action verbs for the moderation dashboard (PATCH /reports/:id). */
|
|
209
|
+
export type ReviewUserReportAction =
|
|
210
|
+
| "ignore"
|
|
211
|
+
| "suspend"
|
|
212
|
+
| "demerit"
|
|
213
|
+
| "deactivate"
|
|
214
|
+
| "delete";
|
|
215
|
+
|
|
216
|
+
interface ReviewIgnore {
|
|
217
|
+
action: "ignore";
|
|
218
|
+
notes?: string;
|
|
219
|
+
}
|
|
220
|
+
interface ReviewSuspend {
|
|
221
|
+
action: "suspend";
|
|
222
|
+
reason: string;
|
|
223
|
+
/** ISO date; if omitted the user is suspended indefinitely. */
|
|
224
|
+
until?: string;
|
|
225
|
+
notes?: string;
|
|
226
|
+
}
|
|
227
|
+
interface ReviewDemerit {
|
|
228
|
+
action: "demerit";
|
|
229
|
+
reason: string;
|
|
230
|
+
/** Defaults to 1 server-side. */
|
|
231
|
+
points?: number;
|
|
232
|
+
notes?: string;
|
|
233
|
+
}
|
|
234
|
+
interface ReviewDeactivate {
|
|
235
|
+
action: "deactivate";
|
|
236
|
+
reason: string;
|
|
237
|
+
notes?: string;
|
|
238
|
+
}
|
|
239
|
+
interface ReviewDelete {
|
|
240
|
+
action: "delete";
|
|
241
|
+
reason: string;
|
|
242
|
+
/** Reviewer must type the reported user's username/email to confirm. */
|
|
243
|
+
confirmation: string;
|
|
244
|
+
notes?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export type ReviewUserReportRequest =
|
|
248
|
+
| ReviewIgnore
|
|
249
|
+
| ReviewSuspend
|
|
250
|
+
| ReviewDemerit
|
|
251
|
+
| ReviewDeactivate
|
|
252
|
+
| ReviewDelete;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* doesn't accidentally re-introduce the same class of bug we just fixed in
|
|
7
7
|
* getFrontendHost (server-only env vars leaking into the browser code path).
|
|
8
8
|
*/
|
|
9
|
-
import { getBackendHost } from "../urlUtils.js";
|
|
9
|
+
import { getBackendHost, getFrontendHost } from "../urlUtils.js";
|
|
10
10
|
|
|
11
11
|
type WindowLocationStub = {
|
|
12
12
|
protocol: string;
|
|
@@ -126,3 +126,30 @@ describe("getBackendHost — Capacitor native (iOS / Android)", () => {
|
|
|
126
126
|
expect(result).toBe(process.env.REACT_APP_API ?? "http://localhost:3500");
|
|
127
127
|
});
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
describe("getFrontendHost — Capacitor native (iOS / Android)", () => {
|
|
131
|
+
it("uses the public web host instead of the WebView localhost origin", () => {
|
|
132
|
+
setWindow(
|
|
133
|
+
{
|
|
134
|
+
protocol: "capacitor:",
|
|
135
|
+
hostname: "localhost",
|
|
136
|
+
host: "localhost",
|
|
137
|
+
port: "",
|
|
138
|
+
},
|
|
139
|
+
{ isNativePlatform: () => true }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(getFrontendHost()).toBe("https://bash.community");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("keeps regular local dev behavior for browser localhost", () => {
|
|
146
|
+
setWindow({
|
|
147
|
+
protocol: "http:",
|
|
148
|
+
hostname: "localhost",
|
|
149
|
+
host: "localhost:3000",
|
|
150
|
+
port: "3000",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(getFrontendHost()).toBe("http://localhost:3000");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
type WindowLocationStub = {
|
|
2
|
+
protocol: string;
|
|
3
|
+
hostname: string;
|
|
4
|
+
host: string;
|
|
5
|
+
port: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type CapacitorStub = { isNativePlatform?: () => boolean };
|
|
9
|
+
|
|
10
|
+
const ORIGINAL_WINDOW = (globalThis as { window?: unknown }).window;
|
|
11
|
+
const ORIGINAL_FRONTEND_URL = process.env.REACT_APP_FRONTEND_URL;
|
|
12
|
+
|
|
13
|
+
function setWindow(loc: WindowLocationStub, capacitor?: CapacitorStub): void {
|
|
14
|
+
(
|
|
15
|
+
globalThis as unknown as {
|
|
16
|
+
window: { location: WindowLocationStub; Capacitor?: CapacitorStub };
|
|
17
|
+
}
|
|
18
|
+
).window = {
|
|
19
|
+
location: loc,
|
|
20
|
+
...(capacitor ? { Capacitor: capacitor } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function clearWindow(): void {
|
|
25
|
+
if (ORIGINAL_WINDOW === undefined) {
|
|
26
|
+
delete (globalThis as { window?: unknown }).window;
|
|
27
|
+
} else {
|
|
28
|
+
(globalThis as { window?: unknown }).window = ORIGINAL_WINDOW;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
clearWindow();
|
|
34
|
+
jest.resetModules();
|
|
35
|
+
if (ORIGINAL_FRONTEND_URL === undefined) {
|
|
36
|
+
delete process.env.REACT_APP_FRONTEND_URL;
|
|
37
|
+
} else {
|
|
38
|
+
process.env.REACT_APP_FRONTEND_URL = ORIGINAL_FRONTEND_URL;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("getFrontendHost — native public URL contract", () => {
|
|
43
|
+
it("returns bash.community for a Capacitor localhost WebView", async () => {
|
|
44
|
+
setWindow(
|
|
45
|
+
{
|
|
46
|
+
protocol: "capacitor:",
|
|
47
|
+
hostname: "localhost",
|
|
48
|
+
host: "localhost",
|
|
49
|
+
port: "",
|
|
50
|
+
},
|
|
51
|
+
{ isNativePlatform: () => true }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const { getFrontendHost } = await import("../urlUtils.js");
|
|
55
|
+
|
|
56
|
+
expect(getFrontendHost()).toBe("https://bash.community");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("honors REACT_APP_FRONTEND_URL for native QA builds", async () => {
|
|
60
|
+
process.env.REACT_APP_FRONTEND_URL = "https://qa.bash.community";
|
|
61
|
+
setWindow(
|
|
62
|
+
{
|
|
63
|
+
protocol: "capacitor:",
|
|
64
|
+
hostname: "localhost",
|
|
65
|
+
host: "localhost",
|
|
66
|
+
port: "",
|
|
67
|
+
},
|
|
68
|
+
{ isNativePlatform: () => true }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const { getFrontendHost } = await import("../urlUtils.js");
|
|
72
|
+
|
|
73
|
+
expect(getFrontendHost()).toBe("https://qa.bash.community");
|
|
74
|
+
});
|
|
75
|
+
});
|