Skip to content

Commit

Permalink
Merge pull request #98 from MinaFoundation/feature/discord-username-p…
Browse files Browse the repository at this point in the history
…roposal

Feature/discord username proposal
  • Loading branch information
iluxonchik authored Dec 20, 2024
2 parents b8fa546 + 09baa1d commit bce49ac
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 54 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pgt-web-app",
"version": "0.1.28",
"version": "0.1.29",
"private": true,
"type": "module",
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
-- First delete all existing proposals
DELETE FROM "Proposal";

/*
Warnings:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:
- You are about to drop the column `discord` on the `Proposal` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "Proposal" DROP COLUMN "discord";
1 change: 0 additions & 1 deletion prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ model Proposal {
deliveryRequirements String @db.Text
securityAndPerformance String @db.Text
budgetRequest Decimal @db.Decimal(16, 2)
discord String @db.VarChar(32)
email String @db.VarChar(254)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
Expand Down
41 changes: 27 additions & 14 deletions src/app/api/proposals/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { NextResponse } from "next/server";
import { ProposalService } from "@/services/ProposalService";
import { UserService } from "@/services/UserService";
import prisma from "@/lib/prisma";
import { getOrCreateUserFromRequest } from "@/lib/auth";
import { ZodError } from "zod";
import logger from "@/logging";
import { ApiResponse } from '@/lib/api-response';
import { AppError } from '@/lib/errors';
import { AuthErrors } from '@/constants/errors';
import { AuthErrors, HTTPStatus } from '@/constants/errors';

const proposalService = new ProposalService(prisma);
const userService = new UserService(prisma);

interface RouteContext {
params: Promise<{
Expand All @@ -20,13 +22,19 @@ export async function GET(request: Request, context: RouteContext) {
try {
const user = await getOrCreateUserFromRequest(request);
if (!user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
return ApiResponse.unauthorized("Please log in to view proposals");
}

const proposal = await prisma.proposal.findUnique({
where: { id: parseInt((await context.params).id) },
include: {
user: true,
user: {
select: {
id: true,
linkId: true,
metadata: true,
}
},
fundingRound: {
include: {
considerationPhase: true,
Expand All @@ -38,28 +46,33 @@ export async function GET(request: Request, context: RouteContext) {
});

if (!proposal) {
return NextResponse.json(
{ error: "Proposal not found" },
{ status: 404 }
);
return ApiResponse.notFound("Proposal not found");
}

// Get user info including linked accounts
const userInfo = await userService.getUserInfo(proposal.user.id);
if (!userInfo) {
throw new AppError("Failed to fetch user information", HTTPStatus.INTERNAL_ERROR);
}

// Check if user is owner or linked user
const isOwner = proposal.userId === user.id || proposal.user.linkId === user.linkId;

// Add access control flags
// Combine proposal data with user info
const response = {
...proposal,
isOwner,
isOwner,
user: {
...proposal.user,
metadata: userInfo.user.metadata,
linkedAccounts: userInfo.linkedAccounts,
},
};

return NextResponse.json(response);
return ApiResponse.success(response);
} catch (error) {
logger.error("Failed to fetch proposal:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
return ApiResponse.error(error);
}
}

Expand Down
27 changes: 0 additions & 27 deletions src/components/CreateProposal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,6 @@ const proposalSchema = z.object({
PV.BUDGET_REQUEST.ERROR_MESSAGES.MAX_VALUE
),

discord: z.string()
.max(PV.DISCORD.MAX, PV.DISCORD.ERROR_MESSAGES.MAX)
.regex(PV.DISCORD.PATTERN, PV.DISCORD.ERROR_MESSAGES.PATTERN),

email: z.string()
.email(PV.EMAIL.ERROR_MESSAGES.FORMAT)
.max(PV.EMAIL.MAX, PV.EMAIL.ERROR_MESSAGES.MAX)
Expand All @@ -76,7 +72,6 @@ export default function CreateProposal({ mode = 'create', proposalId }: Props) {
deliveryRequirements: '',
securityAndPerformance: '',
budgetRequest: '',
discord: '',
email: ''
})
const [errors, setErrors] = useState<ValidationErrors>({})
Expand All @@ -95,7 +90,6 @@ const fetchProposal = async () => {
deliveryRequirements: data.deliveryRequirements,
securityAndPerformance: data.securityAndPerformance,
budgetRequest: data.budgetRequest.toString(),
discord: data.discord,
email: data.email
})
} catch (error) {
Expand Down Expand Up @@ -344,27 +338,6 @@ const fetchProposal = async () => {
)}
</div>

<div className="space-y-4">
<Label htmlFor="discord" className="text-xl font-semibold">
Discord Handle
</Label>
<Input
id="discord"
name="discord"
value={formData.discord}
onChange={handleInputChange}
placeholder="Your Discord username"
className={`bg-muted ${errors.discord ? 'border-red-500' : ''}`}
maxLength={PV.DISCORD.MAX}
/>
{errors.discord && (
<p className="text-sm text-red-500">{errors.discord}</p>
)}
<p className="text-sm text-muted-foreground">
{getRemainingChars('discord', PV.DISCORD.MAX)} characters remaining
</p>
</div>

<div className="space-y-4">
<Label htmlFor="email" className="text-xl font-semibold">
E-mail
Expand Down
49 changes: 43 additions & 6 deletions src/components/ProposalDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,24 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip"
import { useFundingRounds } from "@/hooks/use-funding-rounds"
import type { UserMetadata } from '@/services/UserService'

interface LinkedAccount {
id: string;
authSource: {
type: string;
id: string;
username: string;
};
}

interface ProposalWithAccess extends Proposal {
isOwner: boolean;
user: {
metadata: {
username: string;
};
id: string;
linkId: string;
metadata: UserMetadata;
linkedAccounts: LinkedAccount[];
};
fundingRound?: {
id: string;
Expand Down Expand Up @@ -72,6 +83,12 @@ const fetchProposal = useCallback(async () => {
const response = await fetch(`/api/proposals/${proposalId}`)
if (!response.ok) throw new Error('Failed to fetch proposal')
const data = await response.json()

// Validate that we have the required data
if (!data.user?.metadata?.authSource) {
throw new Error('Invalid proposal data structure')
}

setProposal(data)
} catch (error) {
toast({
Expand Down Expand Up @@ -165,7 +182,7 @@ const fetchProposal = useCallback(async () => {
<div className="border rounded-lg p-6 space-y-6">
<div className="space-y-4">
<h2 className="text-2xl font-bold">{proposal.proposalName}</h2>
<p className="text-muted-foreground">by {proposal.user.metadata.username}</p>
<p className="text-muted-foreground">by {proposal.user.metadata?.username}</p>
<div className="flex gap-2">
<span className="px-2 py-1 rounded-full bg-muted text-sm">
Status: {proposal.status.toLowerCase()}
Expand Down Expand Up @@ -219,8 +236,28 @@ const fetchProposal = useCallback(async () => {

<div>
<h3 className="text-xl font-semibold mb-2">Contact Information</h3>
<p className="text-muted-foreground">Discord: {proposal.discord}</p>
<p className="text-muted-foreground">Email: {proposal.email}</p>
<div className="space-y-2">
{/* Show Discord info if author is a Discord user */}
{proposal.user.metadata.authSource.type === 'discord' ? (
<p className="text-muted-foreground">
Discord: {proposal.user.metadata.authSource.username}
</p>
) : (
/* Check for linked Discord account */
proposal.user.linkedAccounts?.some(account => account.authSource.type === 'discord') ? (
<p className="text-muted-foreground">
Discord: {proposal.user.linkedAccounts.find(account =>
account.authSource.type === 'discord'
)?.authSource.username} (linked account)
</p>
) : (
<p className="text-muted-foreground text-sm italic">
No Discord account linked
</p>
)
)}
<p className="text-muted-foreground">Email: {proposal.email}</p>
</div>
</div>
</>
)}
Expand Down
1 change: 0 additions & 1 deletion src/services/ProposalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export const proposalSchema = z.object({
.string()
.regex(PV.BUDGET_REQUEST.PATTERN)
.transform((val) => parseFloat(val)),
discord: z.string().max(PV.DISCORD.MAX).regex(PV.DISCORD.PATTERN),
email: z.string().email().max(PV.EMAIL.MAX),
});

Expand Down

0 comments on commit bce49ac

Please sign in to comment.