From 0e381eb0f3f9e0a959e0517e0d742c3d6d1ce24d Mon Sep 17 00:00:00 2001 From: Illya Gerasymchuk Date: Mon, 9 Dec 2024 15:02:42 +0000 Subject: [PATCH 1/4] feat: implement proposal status management in admin --- src/app/admin/proposals/page.tsx | 11 + .../funding-rounds/[id]/proposals/route.ts | 78 +++++ .../api/admin/proposals/[id]/status/route.ts | 72 +++++ src/app/api/admin/proposals/route.ts | 59 ++++ src/components/admin/ManageProposals.tsx | 285 ++++++++++++++++++ 5 files changed, 505 insertions(+) create mode 100644 src/app/admin/proposals/page.tsx create mode 100644 src/app/api/admin/funding-rounds/[id]/proposals/route.ts create mode 100644 src/app/api/admin/proposals/[id]/status/route.ts create mode 100644 src/app/api/admin/proposals/route.ts create mode 100644 src/components/admin/ManageProposals.tsx diff --git a/src/app/admin/proposals/page.tsx b/src/app/admin/proposals/page.tsx new file mode 100644 index 0000000..425d1cb --- /dev/null +++ b/src/app/admin/proposals/page.tsx @@ -0,0 +1,11 @@ +import { ManageProposalsComponent } from "@/components/admin/ManageProposals"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Manage Proposals | MEF Admin", + description: "Manage proposal statuses and funding round assignments", +}; + +export default function ManageProposalsPage() { + return ; +} \ No newline at end of file diff --git a/src/app/api/admin/funding-rounds/[id]/proposals/route.ts b/src/app/api/admin/funding-rounds/[id]/proposals/route.ts new file mode 100644 index 0000000..29304ab --- /dev/null +++ b/src/app/api/admin/funding-rounds/[id]/proposals/route.ts @@ -0,0 +1,78 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getOrCreateUserFromRequest } from "@/lib/auth"; +import { AdminService } from "@/services/AdminService"; +import { ApiResponse } from "@/lib/api-response"; +import { AppError } from "@/lib/errors"; +import { AuthErrors } from "@/constants/errors"; +import { UserMetadata } from "@/services"; + +const adminService = new AdminService(prisma); + +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await getOrCreateUserFromRequest(request); + if (!user) { + throw AppError.unauthorized(AuthErrors.UNAUTHORIZED); + } + + // Check if user is admin + const isAdmin = await adminService.checkAdminStatus(user.id, user.linkId); + if (!isAdmin) { + throw AppError.forbidden(AuthErrors.FORBIDDEN); + } + + // Get funding round ID from params + const fundingRoundId = (await params).id; + + // Verify funding round exists + const fundingRound = await prisma.fundingRound.findUnique({ + where: { id: fundingRoundId }, + }); + + if (!fundingRound) { + throw AppError.notFound("Funding round not found"); + } + + // Get proposals for the funding round + const proposals = await prisma.proposal.findMany({ + where: { + fundingRoundId, + }, + include: { + user: { + select: { + metadata: true, + }, + }, + fundingRound: { + select: { + name: true, + }, + }, + }, + orderBy: [ + { status: "asc" }, + { createdAt: "desc" }, + ], + }); + + // Transform the data for the frontend + const transformedProposals = proposals.map(proposal => ({ + id: proposal.id, + proposalName: proposal.proposalName, + status: proposal.status, + budgetRequest: proposal.budgetRequest, + createdAt: proposal.createdAt, + submitter: (proposal.user?.metadata as UserMetadata)?.username || "Unknown", + fundingRound: proposal.fundingRound?.name, + })); + + return ApiResponse.success(transformedProposals); + } catch (error) { + return ApiResponse.error(error); + } +} \ No newline at end of file diff --git a/src/app/api/admin/proposals/[id]/status/route.ts b/src/app/api/admin/proposals/[id]/status/route.ts new file mode 100644 index 0000000..18d55ad --- /dev/null +++ b/src/app/api/admin/proposals/[id]/status/route.ts @@ -0,0 +1,72 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getOrCreateUserFromRequest } from "@/lib/auth"; +import { AdminService } from "@/services/AdminService"; +import { ApiResponse } from "@/lib/api-response"; +import { AppError } from "@/lib/errors"; +import { AuthErrors } from "@/constants/errors"; +import { ProposalStatus } from "@prisma/client"; +import { z } from "zod"; +import { UserMetadata } from "@/services"; + +const adminService = new AdminService(prisma); + +// Validation schema for status update +const updateStatusSchema = z.object({ + status: z.nativeEnum(ProposalStatus), +}); + +export async function PATCH( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const user = await getOrCreateUserFromRequest(request); + if (!user) { + throw AppError.unauthorized(AuthErrors.UNAUTHORIZED); + } + + // Check if user is admin + const isAdmin = await adminService.checkAdminStatus(user.id, user.linkId); + if (!isAdmin) { + throw AppError.forbidden(AuthErrors.FORBIDDEN); + } + + // Validate request body + const body = await request.json(); + const { status } = updateStatusSchema.parse(body); + + // Update proposal status + const updatedProposal = await prisma.proposal.update({ + where: { id: parseInt((await params).id) }, + data: { status }, + include: { + user: { + select: { + metadata: true, + }, + }, + fundingRound: { + select: { + name: true, + }, + }, + }, + }); + + return ApiResponse.success({ + id: updatedProposal.id, + proposalName: updatedProposal.proposalName, + status: updatedProposal.status, + budgetRequest: updatedProposal.budgetRequest, + createdAt: updatedProposal.createdAt, + submitter: (updatedProposal.user?.metadata as UserMetadata)?.username || "Unknown", + fundingRound: updatedProposal.fundingRound?.name, + }); + } catch (error) { + if (error instanceof z.ZodError) { + throw AppError.badRequest("Invalid status value"); + } + return ApiResponse.error(error); + } +} \ No newline at end of file diff --git a/src/app/api/admin/proposals/route.ts b/src/app/api/admin/proposals/route.ts new file mode 100644 index 0000000..4cc97bd --- /dev/null +++ b/src/app/api/admin/proposals/route.ts @@ -0,0 +1,59 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { getOrCreateUserFromRequest } from "@/lib/auth"; +import { AdminService } from "@/services/AdminService"; +import { ApiResponse } from "@/lib/api-response"; +import { AppError } from "@/lib/errors"; +import { AuthErrors } from "@/constants/errors"; +import { UserMetadata } from "@/services"; + +const adminService = new AdminService(prisma); + +export async function GET(request: Request) { + try { + const user = await getOrCreateUserFromRequest(request); + if (!user) { + throw AppError.unauthorized(AuthErrors.UNAUTHORIZED); + } + + // Check if user is admin + const isAdmin = await adminService.checkAdminStatus(user.id, user.linkId); + if (!isAdmin) { + throw AppError.forbidden(AuthErrors.FORBIDDEN); + } + + const proposals = await prisma.proposal.findMany({ + include: { + user: { + select: { + metadata: true, + }, + }, + fundingRound: { + select: { + name: true, + }, + }, + }, + orderBy: [ + { status: "asc" }, + { createdAt: "desc" }, + ], + }); + + // Transform the data for the frontend + const transformedProposals = proposals.map(proposal => ({ + id: proposal.id, + proposalName: proposal.proposalName, + status: proposal.status, + budgetRequest: proposal.budgetRequest, + createdAt: proposal.createdAt, + submitter: (proposal.user?.metadata as UserMetadata)?.username || "Unknown", + fundingRound: proposal.fundingRound?.name, + })); + + return ApiResponse.success(transformedProposals); + } catch (error) { + return ApiResponse.error(error); + } +} \ No newline at end of file diff --git a/src/components/admin/ManageProposals.tsx b/src/components/admin/ManageProposals.tsx new file mode 100644 index 0000000..c7f0d36 --- /dev/null +++ b/src/components/admin/ManageProposals.tsx @@ -0,0 +1,285 @@ +"use client"; +import { useCallback, useEffect, useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ProposalStatus } from "@prisma/client"; +import { useToast } from "@/components/ui/use-toast"; +import { Loader2, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; +import { ApiResponse } from "@/lib/api-response"; + +interface Proposal { + id: number; + proposalName: string; + status: ProposalStatus; + budgetRequest: number; + createdAt: Date; + submitter: string; + fundingRound?: string; +} + +interface FundingRound { + id: string; + name: string; + status: string; +} + +type SortField = 'id' | 'proposalName' | 'submitter' | 'status' | 'budgetRequest' | 'createdAt'; +type SortOrder = 'asc' | 'desc'; + +interface SortConfig { + field: SortField; + order: SortOrder; +} + +export function ManageProposalsComponent() { + const [proposals, setProposals] = useState([]); + const [fundingRounds, setFundingRounds] = useState([]); + const [selectedRound, setSelectedRound] = useState("all"); + const [loading, setLoading] = useState(true); + const [updating, setUpdating] = useState(false); + const [sortConfig, setSortConfig] = useState({ field: 'id', order: 'asc' }); + const { toast } = useToast(); + + // Fetch funding rounds + useEffect(() => { + const fetchFundingRounds = async () => { + try { + const response = await fetch("/api/admin/funding-rounds"); + if (!response.ok) throw new Error("Failed to fetch funding rounds"); + const data = await response.json(); + setFundingRounds(data); + } catch (error) { + toast({ + title: "Error", + description: ApiResponse.Response.errorMessageFromError(error, "Failed to load funding rounds"), + variant: "destructive", + }); + } + }; + fetchFundingRounds(); + }, [toast]); + + // Fetch proposals + const fetchProposals = useCallback(async () => { + try { + setLoading(true); + const url = selectedRound === "all" + ? "/api/admin/proposals" + : `/api/admin/funding-rounds/${selectedRound}/proposals`; + const response = await fetch(url); + if (!response.ok) throw new Error("Failed to fetch proposals"); + const data = await response.json(); + setProposals(data); + } catch (error) { + toast({ + title: "Error", + description: ApiResponse.Response.errorMessageFromError(error, "Failed to load proposals"), + variant: "destructive", + }); + } finally { + setLoading(false); + } + }, [selectedRound, toast]); + + useEffect(() => { + fetchProposals(); + }, [fetchProposals]); + + // Update proposal status + const updateProposalStatus = async (proposalId: number, newStatus: ProposalStatus) => { + try { + setUpdating(true); + const response = await fetch(`/api/admin/proposals/${proposalId}/status`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status: newStatus }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(ApiResponse.Response.errorMessageFromResponse(data, "Failed to update status")); + } + + toast({ + title: "Success", + description: "Proposal status updated successfully", + }); + + // Refresh proposals + fetchProposals(); + } catch (error) { + toast({ + title: "Error", + description: ApiResponse.Response.errorMessageFromError(error, "Failed to update proposal status"), + variant: "destructive", + }); + } finally { + setUpdating(false); + } + }; + + const getStatusBadgeColor = (status: ProposalStatus): string => { + const colors: Record = { + DRAFT: "bg-gray-500", + CONSIDERATION: "bg-blue-500", + DELIBERATION: "bg-purple-500", + VOTING: "bg-yellow-500", + APPROVED: "bg-green-500", + REJECTED: "bg-red-500", + WITHDRAWN: "bg-gray-700", + }; + return colors[status] || "bg-gray-500"; + }; + + // Sorting functions + const handleSort = (field: SortField) => { + setSortConfig(current => ({ + field, + order: current.field === field && current.order === 'asc' ? 'desc' : 'asc' + })); + }; + + const getSortedProposals = (proposals: Proposal[]): Proposal[] => { + return [...proposals].sort((a, b) => { + const { field, order } = sortConfig; + let comparison = 0; + + switch (field) { + case 'id': + comparison = a.id - b.id; + break; + case 'proposalName': + comparison = a.proposalName.localeCompare(b.proposalName); + break; + case 'submitter': + comparison = a.submitter.localeCompare(b.submitter); + break; + case 'status': + comparison = a.status.localeCompare(b.status); + break; + case 'budgetRequest': + comparison = Number(a.budgetRequest) - Number(b.budgetRequest); + break; + case 'createdAt': + comparison = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); + break; + default: + comparison = 0; + } + + return order === 'asc' ? comparison : -comparison; + }); + }; + + const getSortIcon = (field: SortField) => { + if (sortConfig.field !== field) { + return ; + } + return sortConfig.order === 'asc' + ? + : ; + }; + + const sortedProposals = getSortedProposals(proposals); + + return ( + + + Manage Proposals + + +
+ +
+ + {loading ? ( +
+ +
+ ) : ( + + + + handleSort('id')} className="cursor-pointer"> + ID {getSortIcon('id')} + + handleSort('proposalName')} className="cursor-pointer"> + Name {getSortIcon('proposalName')} + + handleSort('submitter')} className="cursor-pointer"> + Submitter {getSortIcon('submitter')} + + handleSort('status')} className="cursor-pointer"> + Status {getSortIcon('status')} + + handleSort('budgetRequest')} className="cursor-pointer"> + Budget ($MINA) {getSortIcon('budgetRequest')} + + handleSort('createdAt')} className="cursor-pointer"> + Created {getSortIcon('createdAt')} + + Actions + + + + {sortedProposals.map((proposal) => ( + + {proposal.id} + {proposal.proposalName} + {proposal.submitter} + + + {proposal.status} + + + {proposal.budgetRequest.toLocaleString()} + + {new Date(proposal.createdAt).toLocaleDateString()} + + + + + + ))} + +
+ )} +
+
+ ); +} \ No newline at end of file From dfe13440730d689ccccac0947612141e3424127a Mon Sep 17 00:00:00 2001 From: Illya Gerasymchuk Date: Mon, 9 Dec 2024 15:03:24 +0000 Subject: [PATCH 2/4] feat: (re)-support Discord auth + error feedback + style adjust --- src/app/api/auth/exchange/route.ts | 59 ++++++-- .../route.ts | 1 - src/app/auth/page.tsx | 128 ++++++++++++++++-- src/hooks/use-submission-phase.ts | 2 +- src/lib/auth/jwt.ts | 2 +- src/services/AdminService.ts | 45 +++++- 6 files changed, 212 insertions(+), 25 deletions(-) rename src/app/api/funding-rounds/[id]/{proposals => submitted-proposals}/route.ts (97%) diff --git a/src/app/api/auth/exchange/route.ts b/src/app/api/auth/exchange/route.ts index 61bcf4e..5131940 100644 --- a/src/app/api/auth/exchange/route.ts +++ b/src/app/api/auth/exchange/route.ts @@ -2,6 +2,8 @@ import { verifyToken, generateTokenPair, setTokenCookies } from "@/lib/auth/jwt" import { AppError } from "@/lib/errors"; import { ApiResponse } from "@/lib/api-response"; import logger from "@/logging"; +import { HTTPStatus } from "@/constants/errors"; +import * as jose from 'jose'; export const runtime = "nodejs"; @@ -10,23 +12,56 @@ export async function POST(request: Request) { const { initialToken } = await request.json(); if (!initialToken) { - throw new AppError("Initial token is required", 400); + throw new AppError("Initial token is required", HTTPStatus.BAD_REQUEST); } - // Verify the initial token - const payload = await verifyToken(initialToken); + try { + // Verify the initial token + const payload = await verifyToken(initialToken); - // Generate new token pair - const { accessToken, refreshToken } = await generateTokenPair( - payload.authSource - ); + // Generate new token pair + const { accessToken, refreshToken } = await generateTokenPair( + payload.authSource + ); + + // Create response and set cookies + const response = ApiResponse.success({ success: true }); + return setTokenCookies(response, accessToken, refreshToken); - // Create response and set cookies - const response = ApiResponse.success({ success: true }); - return setTokenCookies(response, accessToken, refreshToken); + } catch (error) { + // Handle specific JWT errors + if (error instanceof jose.errors.JWTExpired) { + throw new AppError("Authentication token has expired", HTTPStatus.UNAUTHORIZED); + } + if (error instanceof jose.errors.JWTInvalid) { + throw new AppError("Invalid authentication token", HTTPStatus.UNAUTHORIZED); + } + if (error instanceof jose.errors.JWTClaimValidationFailed) { + throw new AppError("Token validation failed", HTTPStatus.UNAUTHORIZED); + } + if (error instanceof jose.errors.JWSSignatureVerificationFailed) { + throw new AppError("Invalid token signature", HTTPStatus.UNAUTHORIZED); + } + + // For any other JWT-related errors + if (error instanceof jose.errors.JOSEError) { + throw new AppError("Authentication token error", HTTPStatus.UNAUTHORIZED); + } + + // Re-throw unknown errors + throw error; + } } catch (error) { - logger.error("Token exchange error:", error); - return ApiResponse.error(error); + logger.error("Token exchange error:", error); + // If it's already an AppError, pass it through + if (error instanceof AppError) { + return ApiResponse.error(error); + } + + // For unexpected errors + return ApiResponse.error( + new AppError("Authentication failed", HTTPStatus.INTERNAL_ERROR) + ); } } \ No newline at end of file diff --git a/src/app/api/funding-rounds/[id]/proposals/route.ts b/src/app/api/funding-rounds/[id]/submitted-proposals/route.ts similarity index 97% rename from src/app/api/funding-rounds/[id]/proposals/route.ts rename to src/app/api/funding-rounds/[id]/submitted-proposals/route.ts index 9b9daad..ed03d33 100644 --- a/src/app/api/funding-rounds/[id]/proposals/route.ts +++ b/src/app/api/funding-rounds/[id]/submitted-proposals/route.ts @@ -2,7 +2,6 @@ import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; import { getOrCreateUserFromRequest } from "@/lib/auth"; import { ProposalStatus, Prisma } from "@prisma/client"; -import type { UserMetadata } from "@/services/UserService"; import logger from "@/logging"; interface FormattedProposal { diff --git a/src/app/auth/page.tsx b/src/app/auth/page.tsx index eed9649..44242d9 100644 --- a/src/app/auth/page.tsx +++ b/src/app/auth/page.tsx @@ -6,10 +6,9 @@ import { useAuth } from '@/contexts/AuthContext'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { ExternalLinkIcon, DiscordLogoIcon, ChatBubbleIcon } from "@radix-ui/react-icons"; -import { Wallet, Loader2 } from 'lucide-react'; -import { WalletConnector } from '@/components/web3/WalletConnector'; +import { Wallet, Loader2, AlertCircle, CheckCircle2 } from 'lucide-react'; import { WalletConnectorDialog } from '@/components/web3/WalletConnectorDialog'; import { WalletAuthDialog } from '@/components/web3/WalletAuthDialog'; import { useWallet } from '@/contexts/WalletContext'; @@ -146,15 +145,126 @@ function AuthenticationOptions() { ); } +function TokenAuthContent() { + const router = useRouter(); + const searchParams = useSearchParams(); + const { refresh } = useAuth(); + const token = searchParams?.get('token'); + const from = searchParams?.get('from') || '/'; + const message = searchParams?.get('message'); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (!token) { + if (message) { + setError(message); + } + return; + } + + async function authenticate() { + setIsLoading(true); + setError(null); + try { + const res = await fetch('/api/auth/exchange', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ initialToken: token }), + credentials: 'include', + }); + + if (!res.ok) { + const data = await res.json(); + throw new Error(data.error || 'Authentication failed'); + } + + await refresh(); + + setSuccess(true); + // Small delay to show success message + setTimeout(() => { + router.push(from); + }, 1000); + } catch (err) { + setError(err instanceof Error ? err.message : 'Authentication failed'); + } finally { + setIsLoading(false); + } + } + + authenticate(); + }, [token, from, router, message, refresh]); + + if (isLoading) { + return ( + + + Authentication + Please wait while we authenticate your session... + + + + + + ); + } + + if (error) { + return ( + + + Authentication Error + Unable to complete authentication + + + + + Error + {error} + + + + ); + } + + if (success) { + return ( + + + Authentication Successful + Redirecting you to your destination... + + + + + Success! + + Authentication completed successfully. + + + + + ); + } + + return null; +} + export default function AuthPage() { const { user, isLoading } = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); + const token = searchParams?.get('token'); useEffect(() => { let mounted = true; const redirect = () => { - if (mounted && !isLoading && user) { + if (mounted && !isLoading && user && !token) { router.push('/'); } }; @@ -164,11 +274,11 @@ export default function AuthPage() { return () => { mounted = false; }; - }, [user, isLoading, router]); + }, [user, isLoading, router, token]); if (isLoading) { return ( -
+
Loading... @@ -182,13 +292,13 @@ export default function AuthPage() { ); } - if (user) { + if (user && !token) { return null; // Will redirect in useEffect } return ( -
- +
+ {token ? : }
); } \ No newline at end of file diff --git a/src/hooks/use-submission-phase.ts b/src/hooks/use-submission-phase.ts index 1a0f750..80351bd 100644 --- a/src/hooks/use-submission-phase.ts +++ b/src/hooks/use-submission-phase.ts @@ -30,7 +30,7 @@ export function useSubmissionPhase(fundingRoundId: string): UseSubmissionPhaseRe async function fetchProposals() { try { const response = await fetch( - `/api/funding-rounds/${fundingRoundId}/proposals` + `/api/funding-rounds/${fundingRoundId}/submitted-proposals` ); if (!response.ok) { diff --git a/src/lib/auth/jwt.ts b/src/lib/auth/jwt.ts index 2080cd7..5501358 100644 --- a/src/lib/auth/jwt.ts +++ b/src/lib/auth/jwt.ts @@ -96,7 +96,7 @@ export const JWTUtils = { } as JWTPayload; } catch (error) { logger.error("Token verification failed:", error); - throw new Error("Invalid token"); + throw error; } }, diff --git a/src/services/AdminService.ts b/src/services/AdminService.ts index 0fe676b..d687458 100644 --- a/src/services/AdminService.ts +++ b/src/services/AdminService.ts @@ -1,4 +1,4 @@ -import { PrismaClient, Prisma } from "@prisma/client"; +import { PrismaClient, Prisma, ProposalStatus } from "@prisma/client"; import type { User, ReviewerGroup, ReviewerGroupMember } from "@prisma/client"; export class AdminService { @@ -676,4 +676,47 @@ export class AdminService { }); }); } + + async getProposalsByFundingRound(fundingRoundId: string) { + return this.prisma.proposal.findMany({ + where: { + fundingRoundId, + }, + include: { + user: { + select: { + metadata: true, + }, + }, + fundingRound: { + select: { + name: true, + }, + }, + }, + orderBy: [ + { status: "asc" }, + { createdAt: "desc" }, + ], + }); + } + + async updateProposalStatus(proposalId: number, status: ProposalStatus) { + return this.prisma.proposal.update({ + where: { id: proposalId }, + data: { status }, + include: { + user: { + select: { + metadata: true, + }, + }, + fundingRound: { + select: { + name: true, + }, + }, + }, + }); + } } From 461546fbc2cab098cda4f3fe8a12af04f84d3cd5 Mon Sep 17 00:00:00 2001 From: Illya Gerasymchuk Date: Mon, 9 Dec 2024 15:07:08 +0000 Subject: [PATCH 3/4] chore: bump verstion to 0.1.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbb69d4..9151111 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pgt-web-app", - "version": "0.1.14", + "version": "0.1.15", "private": true, "type": "module", "scripts": { From c812048884ca0658d5dc1aed28ba35e2e4244458 Mon Sep 17 00:00:00 2001 From: Illya Gerasymchuk Date: Mon, 9 Dec 2024 16:21:34 +0000 Subject: [PATCH 4/4] feat: adapt middlware permissions to allow for public/wallet paths + copy public directory to standalone on build --- Dockerfile | 1 + next.config.ts | 7 ------- package.json | 3 ++- src/middleware.ts | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 next.config.ts diff --git a/Dockerfile b/Dockerfile index e744865..b5218b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,7 @@ COPY tsconfig.json . COPY tailwind.config.ts . COPY postcss.config.mjs . COPY prisma ./prisma/ +COPY public ./public RUN npx prisma generate diff --git a/next.config.ts b/next.config.ts deleted file mode 100644 index e9ffa30..0000000 --- a/next.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ -}; - -export default nextConfig; diff --git a/package.json b/package.json index 9151111..a3c743c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "npm run build:workers && next dev", "dev:https": "NODE_TLS_REJECT_UNAUTHORIZED=0 next dev --experimental-https --experimental-https-key ./localhost-key.pem --experimental-https-cert ./localhost.pem", - "build": "next build && npm run build:workers", + "build": "next build && npm run internal:copy-public && npm run build:workers", + "internal:copy-public": "cp -r public .next/standalone/", "build:workers": "npx tsx src/scripts/build-workers.ts", "start": "npx prisma migrate deploy && next start", "start:prod": "prisma migrate deploy && node server.js", diff --git a/src/middleware.ts b/src/middleware.ts index 6e07701..4adfcd3 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -9,7 +9,7 @@ const getBaseUrl = () => process.env.NEXT_APP_URL; // Configuration export const config = { - matcher: ["/((?!_next/static|_next/image|favicon.ico|public).*)"], + matcher: ["/((?!_next/static|_next/image|favicon.ico|public|wallets).*)"], }; // Route definitions