Skip to content

feat: implement bounty application endpoint#64

Open
genesis-ai-labs-star wants to merge 6 commits intodevasignhq:mainfrom
genesis-ai-labs-star:feat/bounty-application-endpoint
Open

feat: implement bounty application endpoint#64
genesis-ai-labs-star wants to merge 6 commits intodevasignhq:mainfrom
genesis-ai-labs-star:feat/bounty-application-endpoint

Conversation

@genesis-ai-labs-star
Copy link

Goal

Implement POST /bounties/:id/apply to allow users to apply for open bounties.

Changes

  • Added POST /bounties/:id/apply endpoint.
  • Validates bounty ID format (UUID).
  • Checks if bounty exists and is currently 'open'.
  • Prevents duplicate applications from the same user (handles unique constraint errors).
  • Returns 201 Created with the application data on success.

Validation

  • Required field coverLetter checked.
  • Bounty status checked.
  • Duplicate application check (DB unique constraint).

Closes #23

@devasign-app
Copy link

devasign-app bot commented Feb 23, 2026

🔴 AI Code Review Results

Status: Major Issues Found
Confidence: 100%


🔴 Merge Score: 20/100

🔴 ████░░░░░░░░░░░░░░░░ 20%

Recommendation: 🚫 This PR has major issues that must be addressed before merging.

The PR introduces a critical security vulnerability by accepting the applicant's ID from the request body, which must be fixed. Additionally, the new endpoint is completely untested and contains a race condition. These issues must be addressed before merging.

💡 Code Suggestions (4)

🔴 High Priority (2)

  1. packages/api/src/app.ts (Line 148)
    🔧 A critical security vulnerability exists where the applicantId is accepted from the request body. This allows any user to submit an application on behalf of another user by simply providing their ID in the request. The applicant's ID must be derived from a trusted source, such as an authentication middleware that sets the user's context.

💭 Reasoning: Accepting user identifiers from the request body is a form of Insecure Direct Object Reference (IDOR). It compromises data integrity and allows for unauthorized actions. The user's identity should always be established via their authenticated session/token, not client-provided data.

Suggested Code:

        // Assume auth middleware provides the user's ID in the context, e.g., c.get('userId')
        const userId = c.get('userId'); // Replace with your actual auth context retrieval
        if (!userId) {
            return c.json({ error: 'Authentication required' }, 401);
        }

        const body = await c.req.json();
        // Remove applicantId from the destructured body
        const { coverLetter, estimatedTime, experienceLinks } = body;
  1. packages/api/src/tests/app.test.ts (Line 121)
    🔧 The new POST /bounties/:id/apply endpoint lacks any test coverage. All success and failure paths for this critical feature should be tested.

💭 Reasoning: Without tests, there is no guarantee that the endpoint functions as expected or that future changes won't break its functionality. This increases the risk of bugs related to application submission, validation, and error handling.

Suggested Code:

    });

    describe('POST /bounties/:id/apply', () => {
        const bountyId = '11111111-1111-1111-1111-111111111111';
        const applicationPayload = { coverLetter: 'I am the best candidate.' };

        it('should return 401 if user is not authenticated', async () => {
            // Test case for when userId is not available in context
        });

        it('should return 400 for invalid bounty ID format', async () => {
            const res = await app.request('/bounties/invalid-uuid/apply', { method: 'POST' });
            expect(res.status).toBe(400);
        });

        it('should return 404 if bounty does not exist', async () => {
            mockDb.execute.mockResolvedValueOnce({ rows: [] }); // Bounty check fails
            const res = await app.request(`/bounties/${bountyId}/apply`, { method: 'POST', body: JSON.stringify(applicationPayload) });
            expect(res.status).toBe(404);
        });

        it('should return 400 if bounty is not open', async () => {
            mockDb.execute.mockResolvedValueOnce({ rows: [{ status: 'closed' }] }); // Bounty check finds a closed bounty
            const res = await app.request(`/bounties/${bountyId}/apply`, { method: 'POST', body: JSON.stringify(applicationPayload) });
            expect(res.status).toBe(400);
        });

        it('should return 400 if coverLetter is missing', async () => {
            const res = await app.request(`/bounties/${bountyId}/apply`, { method: 'POST', body: JSON.stringify({}) });
            expect(res.status).toBe(400);
        });

        it('should successfully create an application and return 201', async () => {
            mockDb.execute.mockResolvedValueOnce({ rows: [{ status: 'open' }] }); // Bounty check passes
            mockDb.execute.mockResolvedValueOnce({ rows: [{ id: 'app-id', ...applicationPayload }] }); // Insert succeeds
            const res = await app.request(`/bounties/${bountyId}/apply`, { method: 'POST', body: JSON.stringify(applicationPayload) });
            expect(res.status).toBe(201);
        });

        it('should return 400 on duplicate application', async () => {
            mockDb.execute.mockResolvedValueOnce({ rows: [{ status: 'open' }] }); // Bounty check passes
            const duplicateError = new Error('unique constraint violation');
            (duplicateError as any).code = '23505';
            mockDb.execute.mockRejectedValueOnce(duplicateError); // Insert fails with unique constraint
            const res = await app.request(`/bounties/${bountyId}/apply`, { method: 'POST', body: JSON.stringify(applicationPayload) });
            expect(res.status).toBe(400);
        });
    });

🟡 Medium Priority (1)

  1. packages/api/src/app.ts (Line 156)
    🔧 There is a race condition between checking if a bounty is 'open' and inserting the application. The bounty's status could change between the SELECT and INSERT queries, leading to an application being created for a bounty that is no longer open.

💭 Reasoning: To ensure data consistency, the check and the insert operation must be atomic. A database transaction guarantees that both operations succeed or fail together, preventing race conditions and ensuring the application is only created if the bounty is open at the moment of insertion.

Suggested Code:

        // The DB dependency should be updated to support transactions.
        // This is a conceptual example of how it should be structured.
        try {
            const application = await db.transaction(async (tx) => {
                // 1. Check if bounty exists and is open
                const bountyCheck = await tx.execute(sql`SELECT status FROM bounties WHERE id = ${bountyId} FOR UPDATE`);
                if (!bountyCheck.rows?.[0]) {
                    // Throw an error that can be caught to return a 404
                    throw new Error('BountyNotFound');
                }
                if (bountyCheck.rows[0].status !== 'open') {
                    // Throw an error that can be caught to return a 400
                    throw new Error('BountyNotOpen');
                }

                // 2. Submit application
                const q = sql`INSERT INTO applications (...) VALUES (...) RETURNING *;`;
                const result = await tx.execute(q);
                return result.rows[0];
            });

            return c.json(application, 201);
        } catch (err: any) {
            if (err.message === 'BountyNotFound') return c.json({ error: 'Bounty not found' }, 404);
            if (err.message === 'BountyNotOpen') return c.json({ error: 'Bounty is no longer open for applications' }, 400);
            if (err.code === '23505') {
                return c.json({ error: 'You have already applied for this bounty' }, 400);
            }
            throw err;
        }

🔵 Low Priority (1)

  1. packages/api/src/app.ts (Line 176)
    🎨 The error handling for unique constraint violations relies on checking the error message string, which is brittle and can change between database versions or drivers.

💭 Reasoning: Relying on the standardized SQL error code (e.g., '23505' for unique_violation in PostgreSQL) is a more robust and future-proof way to identify specific database errors.

Suggested Code:

        } catch (err: any) {
            // Rely solely on the error code, which is more stable than the message.
            if (err.code === '23505') { // '23505' is the SQLSTATE code for unique_violation
                return c.json({ error: 'You have already applied for this bounty' }, 400);
            }
            throw err;
        }
📊 Review Metadata
  • Processing Time: 88s
  • Analysis Date: 2/23/2026, 2:50:00 AM

🤖 This review was generated by AI. While we strive for accuracy, please use your judgment when applying suggestions.

💬 Questions about this review? Open an issue or contact support.

@devasign-app
Copy link

devasign-app bot commented Feb 23, 2026

🔄 Follow-Up AI Code Review

Status: Major Issues Found
Confidence: 100%


🔴 Updated Merge Score: 40/100

🔴 ████████░░░░░░░░░░░░ 40%

Recommendation: 🚫 This PR has major issues that must be addressed before merging.

📋 Previous Review Summary

The PR introduces a critical security vulnerability by accepting the applicant's ID from the request body, which must be fixed. Additionally, the new endpoint is completely untested and contains a race condition. These issues must be addressed before merging.

The author has added a comprehensive test suite for the new endpoint, which is a great improvement and addresses a key concern from the previous review. However, the most critical issue, the IDOR security vulnerability, remains unfixed; a // TODO comment is not a solution. Additionally, the race condition between checking the bounty status and creating the application also persists. Both of these issues must be resolved before this PR can be considered for merging.

💡 Code Suggestions (2)

🔴 High Priority (1)

  1. packages/api/src/app.ts (Line 148)
    🔧 The critical IDOR (Insecure Direct Object Reference) security vulnerability remains unfixed. The applicantId is still read from the request body, which allows an attacker to submit an application on behalf of any other user. A // TODO comment is not an acceptable fix for a critical vulnerability.

💭 Reasoning: Accepting a user's identifier from the request body is a severe security flaw. The identity of the user performing the action must always be established by the server via a trusted authentication context (e.g., from a session or JWT) and never trusted from client input.

Suggested Code:

        const body = await c.req.json();
        // The applicant's ID MUST be derived from a trusted authentication context.
        // For example: const applicantId = c.get('user').id;
        // The line below must be removed.
        const { coverLetter, estimatedTime, experienceLinks, applicantId } = body;

        if (!coverLetter) {
            return c.json({ error: 'coverLetter is required' }, 400);
        }

        // This check for applicantId from the body must be removed.
        if (!applicantId) {
            return c.json({ error: 'applicantId is required' }, 400);
        }

🟡 Medium Priority (1)

  1. packages/api/src/app.ts (Line 164)
    🔧 A race condition exists between checking if a bounty is 'open' and inserting the application. These two separate database calls create a window where the bounty's status could change, leading to an application being submitted to a bounty that is no longer open.

💭 Reasoning: Separating the state check (Time-of-check) from the state change (Time-of-use) is a classic TOCTOU race condition. This should be performed as a single, atomic database operation to ensure data integrity.

Suggested Code:

        // 2. Submit application atomically
        try {
            const q = sql`
                INSERT INTO applications (
                    bounty_id, 
                    applicant_id, 
                    cover_letter, 
                    estimated_time, 
                    experience_links, 
                    status
                ) 
                SELECT 
                    ${bountyId}, 
                    ${applicantId}, -- This MUST come from auth context
                    ${coverLetter}, 
                    ${estimatedTime || null}, 
                    ${experienceLinks || []}, 
                    'pending'
                FROM bounties
                WHERE id = ${bountyId} AND status = 'open'
                RETURNING *;
            `;
            const result = await db.execute(q);

            if (result.rows.length === 0) {
                // The insert failed, likely because the bounty was not found or not open.
                // You can add a follow-up check here to provide a more specific error message.
                return c.json({ error: 'Bounty not found or is no longer open for applications' }, 400);
            }

            return c.json(result.rows[0], 201);
        } catch (err: any) {
            // Use SQLSTATE code for more robust error handling
            if (err.code === '23505') {
                return c.json({ error: 'You have already applied for this bounty' }, 400);
            }
            throw err;
        }
📊 Review Metadata
  • Processing Time: 80s
  • Analysis Date: 2/23/2026, 2:55:39 AM
  • Review Type: Follow-Up (triggered by new push)

🤖 This is a follow-up review generated after new commits were pushed to the PR.

💬 Questions? Open an issue or contact support.

@devasign-app
Copy link

devasign-app bot commented Feb 23, 2026

🔄 Follow-Up AI Code Review

Status: Changes Needed
Confidence: 100%


🟠 Updated Merge Score: 60/100

🔴 ████████████░░░░░░░░ 60%

Recommendation: ❌ This PR needs significant improvements before it should be merged.

📋 Previous Review Summary

The author has added a comprehensive test suite for the new endpoint, which is a great improvement and addresses a key concern from the previous review. However, the most critical issue, the IDOR security vulnerability, remains unfixed; a // TODO comment is not a solution. Additionally, the race condition between checking the bounty status and creating the application also persists. Both of these issues must be resolved before this PR can be considered for merging.

Excellent work resolving the race condition by using an atomic INSERT ... SELECT query; this is a robust and performant solution. However, the critical IDOR security vulnerability identified in the previous review remains unfixed. The applicantId is still taken from the request body, which is a blocker for merging. This must be derived from a secure authentication context on the server before this PR can be approved.

💡 Code Suggestions (1)

🔴 High Priority (1)

  1. packages/api/src/app.ts (Line 130)
    🔧 The critical Insecure Direct Object Reference (IDOR) vulnerability noted in the previous review still exists. The applicantId is taken directly from the request body, allowing any user to submit an application on behalf of any other user by simply providing their ID.

💭 Reasoning: User identity must always be verified on the server via a secure mechanism like an authentication token or session, not trusted from a client-provided request body. This prevents impersonation and is a fundamental security requirement. The previous // SECURITY comment acknowledging this was also removed, making the vulnerability less apparent.

Suggested Code:

        const body = await c.req.json();
        const { coverLetter, estimatedTime, experienceLinks } = body;

        // The applicant's ID must be retrieved from a secure context (e.g., a JWT set by auth middleware)
        // const applicantId = c.get('userId');
        // For now, since auth is not implemented, we'll add a placeholder but this is NOT secure.
        const { applicantId } = body; // TODO: Replace with authenticated user ID
📊 Review Metadata
  • Processing Time: 74s
  • Analysis Date: 2/23/2026, 3:14:58 AM
  • Review Type: Follow-Up (triggered by new push)

🤖 This is a follow-up review generated after new commits were pushed to the PR.

💬 Questions? Open an issue or contact support.

@bishopBethel bishopBethel self-requested a review February 24, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement POST /bounties/:id/apply — submit application

1 participant