Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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
3 changes: 3 additions & 0 deletions platforms/ereputation/api/src/database/entities/Reference.ts
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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