Skip to content

Commit f9c0f83

Browse files
authored
feat: anonymous ereferences (#927)
* feat: anonymous ereferences * fix: form reset
1 parent 9c528c2 commit f9c0f83

13 files changed

Lines changed: 135 additions & 49 deletions

File tree

platforms/ereputation/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"dev": "nodemon --exec \"npx ts-node\" src/index.ts",
99
"build": "tsc",
1010
"typeorm": "typeorm-ts-node-commonjs",
11-
"migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts",
11+
"migration:generate": "bash -c 'read -p \"Migration name: \" name && npx typeorm-ts-node-commonjs migration:generate src/database/migrations/$name -d src/database/data-source.ts'",
1212
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts",
1313
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/database/data-source.ts"
1414
},

platforms/ereputation/api/src/controllers/DashboardController.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,14 @@ export class DashboardController {
8484
// Add received references (only if filter allows)
8585
if (filter === 'all' || filter === 'received-references') {
8686
receivedReferences.forEach(ref => {
87-
// Get author name, preferring name over ename, with fallback
88-
const authorName = ref.author?.name || ref.author?.ename || ref.author?.handle || 'Unknown';
87+
// Get author name; use "Anonymous" when reference is anonymous
88+
const authorName = ref.anonymous
89+
? 'Anonymous'
90+
: (ref.author?.name || ref.author?.ename || ref.author?.handle || 'Unknown');
91+
// For anonymous refs, exclude author from data to avoid leaking identity
92+
const refData = ref.anonymous
93+
? { ...ref, author: undefined, authorId: undefined }
94+
: ref;
8995
activities.push({
9096
id: `ref-received-${ref.id}`,
9197
type: 'reference',
@@ -94,7 +100,7 @@ export class DashboardController {
94100
targetType: 'user',
95101
date: ref.createdAt,
96102
status: this.mapReferenceStatus(ref.status),
97-
data: ref
103+
data: { ...refData, anonymous: ref.anonymous ?? false }
98104
});
99105
});
100106
}

platforms/ereputation/api/src/controllers/ReferenceController.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export class ReferenceController {
1414

1515
createReference = async (req: Request, res: Response) => {
1616
try {
17-
const { targetType, targetId, targetName, content, referenceType, numericScore } = req.body;
17+
const { targetType, targetId, targetName, content, referenceType, numericScore, anonymous } = req.body;
1818
const authorId = req.user!.id;
1919

2020
if (!targetType || !targetId || !targetName || !content) {
@@ -33,7 +33,8 @@ export class ReferenceController {
3333
content,
3434
referenceType: referenceType || "general",
3535
numericScore,
36-
authorId
36+
authorId,
37+
anonymous: anonymous ?? false
3738
});
3839

3940
// Create signing session for the reference
@@ -96,11 +97,12 @@ export class ReferenceController {
9697
targetType: ref.targetType,
9798
targetId: ref.targetId,
9899
targetName: ref.targetName,
99-
author: ref.author ?{
100+
author: ref.anonymous ? null : (ref.author ? {
100101
id: ref.author.id,
101102
ename: ref.author.ename,
102103
name: ref.author.name
103-
} : null,
104+
} : null),
105+
anonymous: ref.anonymous ?? false,
104106
createdAt: ref.createdAt
105107
}))
106108
});
@@ -123,11 +125,12 @@ export class ReferenceController {
123125
numericScore: ref.numericScore,
124126
referenceType: ref.referenceType,
125127
status: ref.status,
126-
author: {
128+
author: ref.anonymous ? null : (ref.author ? {
127129
id: ref.author.id,
128130
ename: ref.author.ename,
129131
name: ref.author.name
130-
},
132+
} : null),
133+
anonymous: ref.anonymous ?? false,
131134
createdAt: ref.createdAt
132135
}))
133136
});
@@ -194,23 +197,25 @@ export class ReferenceController {
194197
content: ref.content,
195198
status: this.mapStatus(ref.status),
196199
date: ref.createdAt,
200+
anonymous: ref.anonymous ?? false,
197201
})),
198202
...receivedResult.references.map((ref) => ({
199203
id: ref.id,
200204
type: "Received" as const,
201-
forFrom: ref.author?.name || ref.author?.ename || "Unknown",
205+
forFrom: ref.anonymous ? "Anonymous" : (ref.author?.name || ref.author?.ename || "Unknown"),
202206
targetType: ref.targetType,
203207
targetName: ref.targetName,
204208
referenceType: ref.referenceType,
205209
numericScore: ref.numericScore,
206210
content: ref.content,
207211
status: this.mapStatus(ref.status),
208212
date: ref.createdAt,
209-
author: {
210-
id: ref.author?.id,
211-
ename: ref.author?.ename,
212-
name: ref.author?.name,
213-
},
213+
anonymous: ref.anonymous ?? false,
214+
author: ref.anonymous ? null : (ref.author ? {
215+
id: ref.author.id,
216+
ename: ref.author.ename,
217+
name: ref.author.name,
218+
} : null),
214219
})),
215220
];
216221

platforms/ereputation/api/src/database/entities/Reference.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ export class Reference {
3434
@Column({ nullable: true })
3535
status!: string; // "signed", "revoked"
3636

37+
@Column({ default: false })
38+
anonymous!: boolean;
39+
3740
@CreateDateColumn()
3841
createdAt!: Date;
3942

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class AnonErefs1773718262631 implements MigrationInterface {
4+
name = 'AnonErefs1773718262631'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`ALTER TABLE "references" ADD "anonymous" boolean NOT NULL DEFAULT false`);
8+
}
9+
10+
public async down(queryRunner: QueryRunner): Promise<void> {
11+
await queryRunner.query(`ALTER TABLE "references" DROP COLUMN "anonymous"`);
12+
}
13+
14+
}

platforms/ereputation/api/src/services/CalculationService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class CalculationService {
7676
const referencesData = references.map(ref => ({
7777
content: ref.content,
7878
numericScore: ref.numericScore,
79-
author: ref.author.ename || ref.author.name || "Anonymous"
79+
author: ref.anonymous ? "Anonymous" : (ref.author?.ename || ref.author?.name || "Unknown")
8080
}));
8181

8282
// Fetch user's wishlist if userValues is not provided or empty

platforms/ereputation/api/src/services/ReferenceService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ export class ReferenceService {
1717
referenceType: string;
1818
numericScore?: number;
1919
authorId: string;
20+
anonymous?: boolean;
2021
}): Promise<Reference> {
2122
// References start as "pending" and require a signature to become "signed"
2223
const reference = this.referenceRepository.create({
2324
...data,
25+
anonymous: data.anonymous ?? false,
2426
status: "pending"
2527
});
2628
return await this.referenceRepository.save(reference);

platforms/ereputation/api/src/services/VotingReputationService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export class VotingReputationService {
102102
const referencesData = references.map(ref => ({
103103
content: ref.content,
104104
numericScore: ref.numericScore,
105-
author: ref.author.ename || ref.author.name || "Anonymous"
105+
author: ref.anonymous ? "Anonymous" : (ref.author?.ename || ref.author?.name || "Unknown")
106106
}));
107107

108108
memberReferencesMap.set(member.id, referencesData);
@@ -292,7 +292,7 @@ export class VotingReputationService {
292292
const referencesData = references.map(ref => ({
293293
content: ref.content,
294294
numericScore: ref.numericScore,
295-
author: ref.author.ename || ref.author.name || "Anonymous"
295+
author: ref.anonymous ? "Anonymous" : (ref.author?.ename || ref.author?.name || "Unknown")
296296
}));
297297

298298
// Build prompt for AI

platforms/ereputation/api/src/utils/jwt.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import jwt, { JwtPayload } from "jsonwebtoken";
22

3-
// Fail fast if JWT_SECRET is missing
43
if (!process.env.EREPUTATION_JWT_SECRET) {
5-
throw new Error("JWT_SECRET environment variable is required but was not provided. Please set JWT_SECRET in your environment configuration.");
4+
throw new Error("EREPUTATION_JWT_SECRET environment variable is required but was not provided. Please set EREPUTATION_JWT_SECRET in your environment configuration.");
65
}
76

87
const JWT_SECRET = process.env.EREPUTATION_JWT_SECRET;

platforms/ereputation/client/client/src/components/modals/reference-modal.tsx

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
1111
import { Label } from "@/components/ui/label";
1212
import { Input } from "@/components/ui/input";
1313
import { Textarea } from "@/components/ui/textarea";
14+
import { Switch } from "@/components/ui/switch";
1415

1516
interface ReferenceModalProps {
1617
open: boolean;
@@ -61,6 +62,7 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
6162
const [selectedTarget, setSelectedTarget] = useState<any>(null);
6263
const [referenceText, setReferenceText] = useState("");
6364
const [referenceType, setReferenceType] = useState("");
65+
const [anonymous, setAnonymous] = useState(false);
6466
const [signingSession, setSigningSession] = useState<{ sessionId: string; qrData: string; expiresAt: string } | null>(null);
6567
const [signingStatus, setSigningStatus] = useState<"pending" | "connecting" | "signed" | "expired" | "error" | "security_violation">("pending");
6668
const [timeRemaining, setTimeRemaining] = useState<number>(900); // 15 minutes in seconds
@@ -234,18 +236,12 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
234236
};
235237
}, [eventSource]);
236238

237-
// Reset signing state when modal closes
239+
// Reset form and signing state when modal closes (Cancel, backdrop click, escape, etc.)
238240
useEffect(() => {
239241
if (!open) {
240-
if (eventSource) {
241-
eventSource.close();
242-
setEventSource(null);
243-
}
244-
setSigningSession(null);
245-
setSigningStatus("pending");
246-
setTimeRemaining(900);
242+
resetForm();
247243
}
248-
}, [open, eventSource]);
244+
}, [open]);
249245

250246
const formatTime = (seconds: number): string => {
251247
const mins = Math.floor(seconds / 60);
@@ -259,6 +255,7 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
259255
setSelectedTarget(null);
260256
setReferenceText("");
261257
setReferenceType("");
258+
setAnonymous(false);
262259
setSigningSession(null);
263260
setSigningStatus("pending");
264261
setTimeRemaining(900);
@@ -326,7 +323,8 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
326323
targetId: selectedTarget.id,
327324
targetName: selectedTarget.name || selectedTarget.ename || selectedTarget.handle || 'Unknown',
328325
content: referenceText,
329-
referenceType: 'general'
326+
referenceType: 'general',
327+
anonymous
330328
});
331329
};
332330

@@ -564,6 +562,23 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
564562
)}
565563
</div>
566564

565+
{/* Anonymous Toggle */}
566+
<div className="flex items-center justify-between rounded-2xl border-2 border-fig/20 p-4 bg-fig-10">
567+
<div>
568+
<Label htmlFor="anonymous" className="text-sm font-black text-fig">
569+
Post anonymously
570+
</Label>
571+
<p className="text-xs text-fig/70 mt-0.5">
572+
Your name will not be shown to the recipient
573+
</p>
574+
</div>
575+
<Switch
576+
id="anonymous"
577+
checked={anonymous}
578+
onCheckedChange={setAnonymous}
579+
/>
580+
</div>
581+
567582
{/* Reference Text */}
568583
<div>
569584
<Label className="block text-sm font-black text-fig mb-2">

0 commit comments

Comments
 (0)