diff --git a/.changeset/rich-pandas-add.md b/.changeset/rich-pandas-add.md new file mode 100644 index 000000000..7c7dcc532 --- /dev/null +++ b/.changeset/rich-pandas-add.md @@ -0,0 +1,5 @@ +--- +"@blockchain-lab-um/dapp": patch +--- + +Adds campaign issue checks. diff --git a/packages/dapp/src/app/api/campaigns/issue/route.ts b/packages/dapp/src/app/api/campaigns/issue/route.ts index 4ebd7fb38..d504c5f0c 100644 --- a/packages/dapp/src/app/api/campaigns/issue/route.ts +++ b/packages/dapp/src/app/api/campaigns/issue/route.ts @@ -76,21 +76,13 @@ export async function POST(request: NextRequest) { }); } - if (campaign.total && campaign.claimed >= campaign.total) { - return new NextResponse('Campaign is already fully claimed', { - status: 400, - headers: { - ...CORS_HEADERS, - }, - }); - } - const { data: completedRequirements, error: completedRequirementsError } = - await supabase.rpc('get_num_of_users_requirements_by_campaign', { - campaign_id: campaignId, - user_id: user.sub, - }); + const { data: claim, error: claimError } = await supabase + .from('claims') + .select('*') + .eq('campaign_id', campaignId) + .eq('user_id', user.sub); - if (completedRequirementsError) { + if (claimError) { return new NextResponse('Internal Server Error', { status: 500, headers: { @@ -98,13 +90,60 @@ export async function POST(request: NextRequest) { }, }); } - if (completedRequirements !== campaign.requirements.length) { - return new NextResponse('User has not completed all requirements', { - status: 400, - headers: { - ...CORS_HEADERS, - }, - }); + + let claimDate = new Date().toISOString(); + let alreadyClaimed = false; + if (claim.length > 0) { + claimDate = claim[0].claimed_at; + alreadyClaimed = true; + } + + if (!alreadyClaimed) { + const now = new Date(); + const startDateValid = + campaign.start_date && new Date(campaign.start_date) > now; + const endDateValid = + campaign.end_date && new Date(campaign.end_date) < now; + const isCampaignInactive = startDateValid || endDateValid; + const isCampaignFullyClaimed = + campaign.total && campaign.claimed >= campaign.total; + + if (isCampaignInactive) { + return new NextResponse('Campaign is not active', { + status: 400, + headers: { ...CORS_HEADERS }, + }); + } + + if (isCampaignFullyClaimed) { + return new NextResponse('Campaign is already fully claimed', { + status: 400, + headers: { ...CORS_HEADERS }, + }); + } + + const { data: completedRequirements, error: completedRequirementsError } = + await supabase.rpc('get_num_of_users_requirements_by_campaign', { + campaign_id: campaignId, + user_id: user.sub, + }); + + if (completedRequirementsError) { + return new NextResponse('Internal Server Error', { + status: 500, + headers: { + ...CORS_HEADERS, + }, + }); + } + if (completedRequirements !== campaign.requirements.length) { + return new NextResponse('User has not completed all requirements', { + status: 400, + headers: { + ...CORS_HEADERS, + }, + }); + } } const agent = await getAgent(); @@ -135,21 +174,6 @@ export async function POST(request: NextRequest) { }); } - const { data: claim, error: claimError } = await supabase - .from('claims') - .select('*') - .eq('campaign_id', campaignId) - .eq('user_id', user.sub); - - if (claimError) { - return new NextResponse('Internal Server Error', { - status: 500, - headers: { - ...CORS_HEADERS, - }, - }); - } - const controllerKeyId = 'key-1'; const issuerDid = await agent.didManagerImport({ @@ -166,12 +190,8 @@ export async function POST(request: NextRequest) { ], }); - let claimDate = new Date().toISOString(); - if (claim.length > 0) { - claimDate = claim[0].claimed_at; - } let credentialId = claim[0]?.credential_id; - if (claim.length === 0) { + if (!alreadyClaimed) { const { data: insertedClaimData, error: updatedClaimError } = await supabase .from('claims') @@ -194,7 +214,6 @@ export async function POST(request: NextRequest) { }); } - console.error('Error updating claim', updatedClaimError); return new NextResponse('Internal Server Error', { status: 500, headers: { diff --git a/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts b/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts index 966cbf973..5bd633f5f 100644 --- a/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts +++ b/packages/dapp/src/app/api/campaigns/requirements/verify/[id]/route.ts @@ -180,8 +180,15 @@ export async function POST( let credentials: VerifiableCredential[]; try { - credentials = presentation.verifiableCredential.map((credential) => - normalizeCredential(credential) + credentials = presentation.verifiableCredential.reduce( + (result, credential) => { + const normalized = normalizeCredential(credential); + if (normalized?.credentialSubject?.id === did) { + result.push(normalized); + } + return result; + }, + [] as VerifiableCredential[] ); } catch (error) { console.error('Error decoding credentials', error); diff --git a/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx b/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx index dd76c6562..c24a79250 100644 --- a/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx +++ b/packages/dapp/src/components/CampaignsDisplay/RequirementDisplay.tsx @@ -12,6 +12,8 @@ import { useSwitchChain, useVerifyRequirement } from '@/hooks'; import { useAuthStore, useMascaStore, useToastStore } from '@/stores'; import { isError } from '@blockchain-lab-um/masca-connector'; import { shallow } from 'zustand/shallow'; +import { VerifiableCredential } from 'did-jwt-vc'; +import type { W3CVerifiableCredential } from '@veramo/core'; type RequirementProps = { requirement: Tables<'requirements'>; @@ -24,7 +26,14 @@ export const RequirementDisplay = ({ }: RequirementProps) => { const t = useTranslations('RequirementDisplay'); - const token = useAuthStore((state) => state.token); + const { token, isSignedIn, changeIsSignInModalOpen } = useAuthStore( + (state) => ({ + token: state.token, + isSignedIn: state.isSignedIn, + changeIsSignInModalOpen: state.changeIsSignInModalOpen, + }), + shallow + ); const { api, did, didMethod, changeDID, changeCurrDIDMethod } = useMascaStore( (state) => ({ @@ -80,9 +89,18 @@ export const RequirementDisplay = ({ return; } - // Create a presentation from all the user's credentials + // Create a presentation from all the user's credentials except the polygonid ones const createPresentationResult = await api.createPresentation({ - vcs: queryCredentialsResult.data.map((queryResult) => queryResult.data), + vcs: queryCredentialsResult.data.reduce((acc, queryResult) => { + const credential = queryResult.data; + if ( + !credential.issuer.includes('did:poylgonid') && + !credential.issuer.includes('did:iden3') + ) { + acc.push(credential); + } + return acc; + }, [] as W3CVerifiableCredential[]), proofFormat: 'EthereumEip712Signature2021', }); @@ -91,6 +109,7 @@ export const RequirementDisplay = ({ return; } setStartedVerifying(false); + await verifyRequirement({ did: currentDid, presentation: createPresentationResult.data, @@ -120,7 +139,9 @@ export const RequirementDisplay = ({