Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion platforms/ereputation/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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 }
});
});
}
Expand Down
29 changes: 17 additions & 12 deletions platforms/ereputation/api/src/controllers/ReferenceController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -33,7 +33,8 @@ export class ReferenceController {
content,
referenceType: referenceType || "general",
numericScore,
authorId
authorId,
anonymous: anonymous ?? false
});

// Create signing session for the reference
Expand Down Expand Up @@ -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
}))
});
Expand All @@ -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
}))
});
Expand Down Expand Up @@ -194,23 +197,25 @@ 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,
numericScore: ref.numericScore,
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),
})),
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export class Reference {
@Column({ nullable: true })
status!: string; // "signed", "revoked"

@Column({ default: false })
anonymous!: boolean;

@CreateDateColumn()
createdAt!: Date;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class AnonErefs1773718262631 implements MigrationInterface {
name = 'AnonErefs1773718262631'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "references" ADD "anonymous" boolean NOT NULL DEFAULT false`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "references" DROP COLUMN "anonymous"`);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions platforms/ereputation/api/src/services/ReferenceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export class ReferenceService {
referenceType: string;
numericScore?: number;
authorId: string;
anonymous?: boolean;
}): Promise<Reference> {
// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions platforms/ereputation/api/src/utils/jwt.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +62,7 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
const [selectedTarget, setSelectedTarget] = useState<any>(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<number>(900); // 15 minutes in seconds
Expand Down Expand Up @@ -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);
Expand All @@ -259,6 +255,7 @@ export default function ReferenceModal({ open, onOpenChange }: ReferenceModalPro
setSelectedTarget(null);
setReferenceText("");
setReferenceType("");
setAnonymous(false);
setSigningSession(null);
setSigningStatus("pending");
setTimeRemaining(900);
Expand Down Expand Up @@ -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
});
};

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

{/* Anonymous Toggle */}
<div className="flex items-center justify-between rounded-2xl border-2 border-fig/20 p-4 bg-fig-10">
<div>
<Label htmlFor="anonymous" className="text-sm font-black text-fig">
Post anonymously
</Label>
<p className="text-xs text-fig/70 mt-0.5">
Your name will not be shown to the recipient
</p>
</div>
<Switch
id="anonymous"
checked={anonymous}
onCheckedChange={setAnonymous}
/>
</div>

{/* Reference Text */}
<div>
<Label className="block text-sm font-black text-fig mb-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,13 @@ export default function ReferenceViewModal({ open, onOpenChange, reference }: Re
</svg>
</div>
<DialogTitle className="text-lg sm:text-xl font-black text-fig">
eReference {reference?.type === 'Sent' ? 'for' : 'from'} {reference?.forFrom}
eReference {reference?.type === 'Sent' ? 'for' : 'from'}{' '}
{reference?.forFrom}
{reference?.anonymous && (
<span className="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-fig/10 text-fig border border-fig/20">
Anonymous
</span>
)}
</DialogTitle>
<DialogDescription className="text-fig/70 text-sm font-medium mt-2">
Professional reference details and status
Expand Down
Loading
Loading