diff --git a/platforms/ereputation/api/package.json b/platforms/ereputation/api/package.json index 9c42c2659..9809baab1 100644 --- a/platforms/ereputation/api/package.json +++ b/platforms/ereputation/api/package.json @@ -8,7 +8,7 @@ "dev": "nodemon --exec \"npx ts-node\" src/index.ts", "build": "tsc", "typeorm": "typeorm-ts-node-commonjs", - "migration:generate": "typeorm-ts-node-commonjs migration:generate -d src/database/data-source.ts", + "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'", "migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts", "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/database/data-source.ts" }, diff --git a/platforms/ereputation/api/src/controllers/DashboardController.ts b/platforms/ereputation/api/src/controllers/DashboardController.ts index 4f140b7e7..7c804f5bc 100644 --- a/platforms/ereputation/api/src/controllers/DashboardController.ts +++ b/platforms/ereputation/api/src/controllers/DashboardController.ts @@ -84,8 +84,14 @@ export class DashboardController { // Add received references (only if filter allows) if (filter === 'all' || filter === 'received-references') { receivedReferences.forEach(ref => { - // Get author name, preferring name over ename, with fallback - const authorName = ref.author?.name || ref.author?.ename || ref.author?.handle || 'Unknown'; + // Get author name; use "Anonymous" when reference is anonymous + const authorName = ref.anonymous + ? 'Anonymous' + : (ref.author?.name || ref.author?.ename || ref.author?.handle || 'Unknown'); + // For anonymous refs, exclude author from data to avoid leaking identity + const refData = ref.anonymous + ? { ...ref, author: undefined, authorId: undefined } + : ref; activities.push({ id: `ref-received-${ref.id}`, type: 'reference', @@ -94,7 +100,7 @@ export class DashboardController { targetType: 'user', date: ref.createdAt, status: this.mapReferenceStatus(ref.status), - data: ref + data: { ...refData, anonymous: ref.anonymous ?? false } }); }); } diff --git a/platforms/ereputation/api/src/controllers/ReferenceController.ts b/platforms/ereputation/api/src/controllers/ReferenceController.ts index ee23340fb..53c95c966 100644 --- a/platforms/ereputation/api/src/controllers/ReferenceController.ts +++ b/platforms/ereputation/api/src/controllers/ReferenceController.ts @@ -14,7 +14,7 @@ export class ReferenceController { createReference = async (req: Request, res: Response) => { try { - const { targetType, targetId, targetName, content, referenceType, numericScore } = req.body; + const { targetType, targetId, targetName, content, referenceType, numericScore, anonymous } = req.body; const authorId = req.user!.id; if (!targetType || !targetId || !targetName || !content) { @@ -33,7 +33,8 @@ export class ReferenceController { content, referenceType: referenceType || "general", numericScore, - authorId + authorId, + anonymous: anonymous ?? false }); // Create signing session for the reference @@ -96,11 +97,12 @@ export class ReferenceController { targetType: ref.targetType, targetId: ref.targetId, targetName: ref.targetName, - author: ref.author ?{ + author: ref.anonymous ? null : (ref.author ? { id: ref.author.id, ename: ref.author.ename, name: ref.author.name - } : null, + } : null), + anonymous: ref.anonymous ?? false, createdAt: ref.createdAt })) }); @@ -123,11 +125,12 @@ export class ReferenceController { numericScore: ref.numericScore, referenceType: ref.referenceType, status: ref.status, - author: { + author: ref.anonymous ? null : (ref.author ? { id: ref.author.id, ename: ref.author.ename, name: ref.author.name - }, + } : null), + anonymous: ref.anonymous ?? false, createdAt: ref.createdAt })) }); @@ -194,11 +197,12 @@ export class ReferenceController { content: ref.content, status: this.mapStatus(ref.status), date: ref.createdAt, + anonymous: ref.anonymous ?? false, })), ...receivedResult.references.map((ref) => ({ id: ref.id, type: "Received" as const, - forFrom: ref.author?.name || ref.author?.ename || "Unknown", + forFrom: ref.anonymous ? "Anonymous" : (ref.author?.name || ref.author?.ename || "Unknown"), targetType: ref.targetType, targetName: ref.targetName, referenceType: ref.referenceType, @@ -206,11 +210,12 @@ export class ReferenceController { content: ref.content, status: this.mapStatus(ref.status), date: ref.createdAt, - author: { - id: ref.author?.id, - ename: ref.author?.ename, - name: ref.author?.name, - }, + anonymous: ref.anonymous ?? false, + author: ref.anonymous ? null : (ref.author ? { + id: ref.author.id, + ename: ref.author.ename, + name: ref.author.name, + } : null), })), ]; diff --git a/platforms/ereputation/api/src/database/entities/Reference.ts b/platforms/ereputation/api/src/database/entities/Reference.ts index 944521234..34a0bac25 100644 --- a/platforms/ereputation/api/src/database/entities/Reference.ts +++ b/platforms/ereputation/api/src/database/entities/Reference.ts @@ -34,6 +34,9 @@ export class Reference { @Column({ nullable: true }) status!: string; // "signed", "revoked" + @Column({ default: false }) + anonymous!: boolean; + @CreateDateColumn() createdAt!: Date; diff --git a/platforms/ereputation/api/src/database/migrations/1773718262631-anon-erefs.ts b/platforms/ereputation/api/src/database/migrations/1773718262631-anon-erefs.ts new file mode 100644 index 000000000..761828d46 --- /dev/null +++ b/platforms/ereputation/api/src/database/migrations/1773718262631-anon-erefs.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AnonErefs1773718262631 implements MigrationInterface { + name = 'AnonErefs1773718262631' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "references" ADD "anonymous" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "references" DROP COLUMN "anonymous"`); + } + +} diff --git a/platforms/ereputation/api/src/services/CalculationService.ts b/platforms/ereputation/api/src/services/CalculationService.ts index 54e7e2ced..77a1c48ef 100644 --- a/platforms/ereputation/api/src/services/CalculationService.ts +++ b/platforms/ereputation/api/src/services/CalculationService.ts @@ -76,7 +76,7 @@ export class CalculationService { const referencesData = references.map(ref => ({ content: ref.content, numericScore: ref.numericScore, - author: ref.author.ename || ref.author.name || "Anonymous" + author: ref.anonymous ? "Anonymous" : (ref.author?.ename || ref.author?.name || "Unknown") })); // Fetch user's wishlist if userValues is not provided or empty diff --git a/platforms/ereputation/api/src/services/ReferenceService.ts b/platforms/ereputation/api/src/services/ReferenceService.ts index 8b983a03a..e6292485a 100644 --- a/platforms/ereputation/api/src/services/ReferenceService.ts +++ b/platforms/ereputation/api/src/services/ReferenceService.ts @@ -17,10 +17,12 @@ export class ReferenceService { referenceType: string; numericScore?: number; authorId: string; + anonymous?: boolean; }): Promise { // References start as "pending" and require a signature to become "signed" const reference = this.referenceRepository.create({ ...data, + anonymous: data.anonymous ?? false, status: "pending" }); return await this.referenceRepository.save(reference); diff --git a/platforms/ereputation/api/src/services/VotingReputationService.ts b/platforms/ereputation/api/src/services/VotingReputationService.ts index 0b1292d8b..124ce16e8 100644 --- a/platforms/ereputation/api/src/services/VotingReputationService.ts +++ b/platforms/ereputation/api/src/services/VotingReputationService.ts @@ -102,7 +102,7 @@ export class VotingReputationService { const referencesData = references.map(ref => ({ content: ref.content, numericScore: ref.numericScore, - author: ref.author.ename || ref.author.name || "Anonymous" + author: ref.anonymous ? "Anonymous" : (ref.author?.ename || ref.author?.name || "Unknown") })); memberReferencesMap.set(member.id, referencesData); @@ -292,7 +292,7 @@ export class VotingReputationService { const referencesData = references.map(ref => ({ content: ref.content, numericScore: ref.numericScore, - author: ref.author.ename || ref.author.name || "Anonymous" + author: ref.anonymous ? "Anonymous" : (ref.author?.ename || ref.author?.name || "Unknown") })); // Build prompt for AI diff --git a/platforms/ereputation/api/src/utils/jwt.ts b/platforms/ereputation/api/src/utils/jwt.ts index f8232e250..ac99c854f 100644 --- a/platforms/ereputation/api/src/utils/jwt.ts +++ b/platforms/ereputation/api/src/utils/jwt.ts @@ -1,8 +1,7 @@ import jwt, { JwtPayload } from "jsonwebtoken"; -// Fail fast if JWT_SECRET is missing if (!process.env.EREPUTATION_JWT_SECRET) { - throw new Error("JWT_SECRET environment variable is required but was not provided. Please set JWT_SECRET in your environment configuration."); + throw new Error("EREPUTATION_JWT_SECRET environment variable is required but was not provided. Please set EREPUTATION_JWT_SECRET in your environment configuration."); } const JWT_SECRET = process.env.EREPUTATION_JWT_SECRET; diff --git a/platforms/ereputation/client/client/src/components/modals/reference-modal.tsx b/platforms/ereputation/client/client/src/components/modals/reference-modal.tsx index 183c344d3..f175aa43d 100644 --- a/platforms/ereputation/client/client/src/components/modals/reference-modal.tsx +++ b/platforms/ereputation/client/client/src/components/modals/reference-modal.tsx @@ -11,6 +11,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; +import { Switch } from "@/components/ui/switch"; interface ReferenceModalProps { open: boolean; @@ -61,6 +62,7 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro const [selectedTarget, setSelectedTarget] = useState(null); const [referenceText, setReferenceText] = useState(""); const [referenceType, setReferenceType] = useState(""); + const [anonymous, setAnonymous] = useState(false); const [signingSession, setSigningSession] = useState<{ sessionId: string; qrData: string; expiresAt: string } | null>(null); const [signingStatus, setSigningStatus] = useState<"pending" | "connecting" | "signed" | "expired" | "error" | "security_violation">("pending"); const [timeRemaining, setTimeRemaining] = useState(900); // 15 minutes in seconds @@ -234,18 +236,12 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro }; }, [eventSource]); - // Reset signing state when modal closes + // Reset form and signing state when modal closes (Cancel, backdrop click, escape, etc.) useEffect(() => { if (!open) { - if (eventSource) { - eventSource.close(); - setEventSource(null); - } - setSigningSession(null); - setSigningStatus("pending"); - setTimeRemaining(900); + resetForm(); } - }, [open, eventSource]); + }, [open]); const formatTime = (seconds: number): string => { const mins = Math.floor(seconds / 60); @@ -259,6 +255,7 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro setSelectedTarget(null); setReferenceText(""); setReferenceType(""); + setAnonymous(false); setSigningSession(null); setSigningStatus("pending"); setTimeRemaining(900); @@ -326,7 +323,8 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro targetId: selectedTarget.id, targetName: selectedTarget.name || selectedTarget.ename || selectedTarget.handle || 'Unknown', content: referenceText, - referenceType: 'general' + referenceType: 'general', + anonymous }); }; @@ -564,6 +562,23 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro )} + {/* Anonymous Toggle */} +
+
+ +

+ Your name will not be shown to the recipient +

+
+ +
+ {/* Reference Text */}
- eReference {reference?.type === 'Sent' ? 'for' : 'from'} {reference?.forFrom} + eReference {reference?.type === 'Sent' ? 'for' : 'from'}{' '} + {reference?.forFrom} + {reference?.anonymous && ( + + Anonymous + + )} Professional reference details and status diff --git a/platforms/ereputation/client/client/src/pages/dashboard.tsx b/platforms/ereputation/client/client/src/pages/dashboard.tsx index 897cfa6d6..6036ec130 100644 --- a/platforms/ereputation/client/client/src/pages/dashboard.tsx +++ b/platforms/ereputation/client/client/src/pages/dashboard.tsx @@ -123,17 +123,21 @@ export default function Dashboard() { // For reference activities, show reference details modal if (activity.type === 'reference' || activity.activity === 'Reference Provided' || activity.activity === 'Reference Received') { const referenceData = activity.data; // This contains the full reference object + const forFromReceived = referenceData?.anonymous + ? 'Anonymous' + : (activity.data?.author?.name || activity.data?.author?.ename || activity.data?.author?.handle || 'Unknown'); setReferenceViewModal({ id: activity.id, type: activity.activity === 'Reference Received' ? 'Received' : 'Sent', forFrom: activity.activity === 'Reference Received' - ? (activity.data?.author?.name || activity.data?.author?.ename || activity.data?.author?.handle || 'Unknown') + ? forFromReceived : activity.target, date: new Date(activity.date).toLocaleDateString(), status: activity.status || 'Unknown', referenceType: referenceData?.referenceType || 'general', content: referenceData?.content || 'Reference content not available', - targetType: referenceData?.targetType || 'user' + targetType: referenceData?.targetType || 'user', + anonymous: referenceData?.anonymous ?? false }); return; } @@ -143,17 +147,21 @@ export default function Dashboard() { if (activity.type === 'reference' || activity.activity === 'Reference Provided' || activity.activity === 'Reference Received') { // This shouldn't happen, but if it does, show reference modal instead const referenceData = activity.data; + const forFromReceived = referenceData?.anonymous + ? 'Anonymous' + : (activity.data?.author?.name || activity.data?.author?.ename || activity.data?.author?.handle || 'Unknown'); setReferenceViewModal({ id: activity.id, type: activity.activity === 'Reference Received' ? 'Received' : 'Sent', forFrom: activity.activity === 'Reference Received' - ? (activity.data?.author?.name || activity.data?.author?.ename || activity.data?.author?.handle || 'Unknown') + ? forFromReceived : activity.target, date: new Date(activity.date).toLocaleDateString(), status: activity.status || 'Unknown', referenceType: referenceData?.referenceType || 'general', content: referenceData?.content || 'Reference content not available', - targetType: referenceData?.targetType || 'user' + targetType: referenceData?.targetType || 'user', + anonymous: referenceData?.anonymous ?? false }); return; } @@ -579,11 +587,18 @@ export default function Dashboard() { - {activity.activity === 'Reference Received' - ? `From ${activity.target || 'Unknown'}` - : activity.activity === 'Reference Provided' - ? `To ${activity.target || 'Unknown'}` - : activity.target || 'Unknown'} +
+ {activity.activity === 'Reference Received' + ? `From ${activity.target || 'Unknown'}` + : activity.activity === 'Reference Provided' + ? `To ${activity.target || 'Unknown'}` + : activity.target || 'Unknown'} + {activity.activity === 'Reference Received' && activity.data?.anonymous && ( + + Anonymous + + )} +
{new Date(activity.date).toLocaleDateString()} @@ -658,12 +673,19 @@ export default function Dashboard() {
{activity.activity}
-
- {activity.activity === 'Reference Received' - ? `From ${activity.target || 'Unknown'}` - : activity.activity === 'Reference Provided' - ? `To ${activity.target || 'Unknown'}` - : activity.target || 'Unknown'} +
+ + {activity.activity === 'Reference Received' + ? `From ${activity.target || 'Unknown'}` + : activity.activity === 'Reference Provided' + ? `To ${activity.target || 'Unknown'}` + : activity.target || 'Unknown'} + + {activity.activity === 'Reference Received' && activity.data?.anonymous && ( + + Anonymous + + )}
diff --git a/platforms/ereputation/client/client/src/pages/references.tsx b/platforms/ereputation/client/client/src/pages/references.tsx index c5622e340..fdba0d4aa 100644 --- a/platforms/ereputation/client/client/src/pages/references.tsx +++ b/platforms/ereputation/client/client/src/pages/references.tsx @@ -243,7 +243,14 @@ export default function References() { - {reference.forFrom} +
+ {reference.forFrom} + {reference.anonymous && ( + + Anonymous + + )} +
{reference.date} @@ -374,7 +381,14 @@ export default function References() {
{reference.type === 'Received' ? 'From' : 'To'}
-
{reference.forFrom}
+
+ {reference.forFrom} + {reference.anonymous && ( + + Anonymous + + )} +