Skip to content
Open
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ next-env.d.ts

supabase/
CLAUDE.md
.mcp.json
.mcp.json
/lib/generated/prisma
/.vscode/
54 changes: 37 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Pakistan's Premier Participatory Democracy Platform**

Hum Awaz (ہم آواز) empowers Pakistani citizens to actively participate in democratic processes through digital consultations, proposal submissions, and collaborative decision-making. Built for transparency, accessibility, and meaningful civic engagement.
Hum Awaz (حم آواز) empowers Pakistani citizens to actively participate in democratic processes through digital consultations, proposal submissions, and collaborative decision-making. Built for transparency, accessibility, and meaningful civic engagement.

---

Expand Down Expand Up @@ -73,7 +73,7 @@ Hum Awaz (ہم آواز) empowers Pakistani citizens to actively participate in
### **Database Schema**
```
profiles → User profiles and preferences
processes → Democratic consultation processes
processes → Democratic consultation processes
proposals → Citizen-submitted proposals
discussions → Comments and conversations
votes → User votes on proposals
Expand All @@ -93,7 +93,6 @@ notifications → User alerts and updates

### **Prerequisites**
- Node.js 18+ and pnpm
- Supabase account and project
- Git for version control

### **Installation**
Expand All @@ -112,21 +111,41 @@ notifications → User alerts and updates
3. **Environment Setup**
Create `.env.local` file:
```env
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
DATABASE_URL="postgresql://postgres:123@localhost:5432/hamnawa?schema=public"

NEXT_PUBLIC_APP_URL="http://localhost:3000"
NEXT_PUBLIC_APP_API_URL="/api"

# SECRET STRINGS
NEXT_JWT_ACCESS_SECRET=""
NEXT_JWT_REFRESH_SECRET=""

NEXT_PUBLIC_APIS="/api/auth/login,/api/auth/register,/api/auth/refresh,/api/public/"
NEXT_PUBLIC_ENDPOINTS="/auth/login,/auth/register,/auth/refresh,/public/"

# FOR GOOGLE OAUTH IF APPLICABLE
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
NODE_ENV='development'

```

4. **Database Setup**
```bash
# Run database migrations in order:
# 1. Drop existing tables (if any)
psql -h your-db-host -d your-db -f supabase/step1-drop-tables.sql

# 2. Create schema and tables
psql -h your-db-host -d your-db -f supabase/step2-create-schema.sql

# 3. Insert sample data
psql -h your-db-host -d your-db -f supabase/step3-sample-data.sql
# Reset the database
pnpx prisma db push --force-reset

# Generate the migrations
pnpx prisma generate

# Push the migrations to the DB
pnpx prisma db push

# For seeding the development environment
pnpx tsx prisma/seed.ts

# For seeding the production environment if applicable
pnpm run db:seed:prod
```

5. **Run Development Server**
Expand All @@ -137,6 +156,7 @@ notifications → User alerts and updates
6. **Open Application**
Navigate to [http://localhost:3000](http://localhost:3000)


---

## 📁 Project Structure
Expand Down Expand Up @@ -275,7 +295,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file

Hum Awaz is designed for collaboration with:
- **Federal Government**: National policy consultations
- **Provincial Governments**: Regional governance initiatives
- **Provincial Governments**: Regional governance initiatives
- **Local Governments**: Community-level decision making
- **Civil Society**: NGO and advocacy group engagement
- **Academic Institutions**: Research and analysis partnerships
Expand Down Expand Up @@ -307,7 +327,7 @@ Hum Awaz envisions a Pakistan where every citizen has a meaningful voice in gove

Our platform bridges the gap between citizens and government, fostering:
- **Active Civic Engagement**: Beyond voting to continuous participation
- **Transparent Governance**: Open processes and accountable outcomes
- **Transparent Governance**: Open processes and accountable outcomes
- **Inclusive Decision-Making**: Voices from all communities and backgrounds
- **Evidence-Based Policy**: Data-driven insights for better governance

Expand Down Expand Up @@ -342,7 +362,7 @@ Special thanks to:

*Built with ❤️ for the people of Pakistan*

**Hum Awaz - ہم آواز - Voice of the People**
**Hum Awaz - حم آواز - Voice of the People**

---

Expand Down
6 changes: 6 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import NextAuth from "next-auth"
import { authOptions } from "@/lib/auth"

const handler = NextAuth(authOptions)

export { handler as GET, handler as POST }
40 changes: 40 additions & 0 deletions app/api/auth/refresh/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// app/api/auth/refresh/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyRefreshToken, generateAccessToken } from '@/lib/jwt';

export async function POST(request: NextRequest) {
try {
const { refreshToken } = await request.json();

if (!refreshToken) {
return NextResponse.json(
{ error: 'Refresh token is required' },
{ status: 400 }
);
}

// Verify the refresh token
const payload = verifyRefreshToken(refreshToken);

// Generate new access token
const newAccessToken = generateAccessToken({
userId: payload.userId,
email: payload.email,
isSuperuser: payload.isSuperuser,
isStaff: payload.isStaff,
permissions: payload.permissions,
});

return NextResponse.json({
accessToken: newAccessToken,
message: 'Token refreshed successfully',
});

} catch (error) {
console.error('Token refresh error:', error);
return NextResponse.json(
{ error: 'Invalid or expired refresh token' },
{ status: 401 }
);
}
}
60 changes: 60 additions & 0 deletions app/api/auth/register/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import prisma from '@/lib/prisma';
import { NextRequest, NextResponse } from 'next/server';
import type { NextApiRequest, NextApiResponse } from 'next';
import { registerFormSchema } from '@/lib/forms';
import bcrypt from 'bcryptjs';

export async function POST(request: NextRequest) {
console.log('[INFO] Inside register route');
try {
const rawData = await request.json();
const result = registerFormSchema.safeParse(rawData);

if (!result.success) {
const fieldErrors = result.error.flatten().fieldErrors;
return NextResponse.json(
{
error: 'Validation failed',
fieldErrors: fieldErrors,
},
{ status: 400 }
);
}

const { full_name, email, password } = result.data;

const existingUser = await prisma.user.findUnique({
where: { email },
});

if (existingUser) {
return NextResponse.json(
{
"error": "User with this email already exists",
},
{ status: 409 }
);
}

const hashedPassword = await bcrypt.hash(password, 10);

const newUser = await prisma.user.create({
data: {
name: full_name,
email,
password: hashedPassword,
},
});

return NextResponse.json(
{ message: 'User registered successfully', userId: newUser.id },
{ status: 201 }
);
} catch (error) {
console.error('Login error:', error);
return NextResponse.json(
{ error: 'Internal server error', fieldErrors: {} },
{ status: 500 }
);
}
}
138 changes: 138 additions & 0 deletions app/api/comments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// app/api/processes/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '@/lib/auth';
import prisma from '@/lib/prisma';
import { discussionFormSchema, processFormSchema } from '@/lib/forms';

export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);

if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { searchParams } = new URL(request.url);

// Extract query parameters
const search = searchParams.get('search') || '';
const category = searchParams.get('category') || '';

// Build where conditions
const whereConditions: any = {
status: { in: ['active', 'closed'] },
};

// Add search filter
if (search.trim()) {
whereConditions.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ description: { contains: search, mode: 'insensitive' } },
];
}

// Add category filter - only if category is specified and not 'all'
if (category.trim() && category !== 'all') {
whereConditions.category = { equals: category, mode: 'insensitive' };
}

const processes = await prisma.process.findMany({
where: whereConditions,
orderBy: { created_at: 'desc' },
include: {
proposals: {
include: {
discussions: true,
},
},
},
});

return NextResponse.json(processes);
}

export async function POST(request: NextRequest) {
console.log('[INFO] Inside POST comment route');
try {
const session = await getServerSession(authOptions);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const rawData = await request.json();
const result = discussionFormSchema.safeParse(rawData);

if (!result.success) {
const fieldErrors = result.error.flatten().fieldErrors;
return NextResponse.json(
{
error: 'Validation failed',
fieldErrors: fieldErrors,
},
{ status: 400 }
);
}

const { comment, proposal_id, process_id } = result.data;

const new_comment = await prisma.discussion.create({
data: {
content: comment,
proposal: proposal_id
? {
connect: { id: proposal_id },
}
: undefined,
process: process_id
? {
connect: { id: process_id },
}
: undefined,
author: {
connect: { id: Number(session.user.id) },
},
},
});

const proposal = await prisma.proposal.findUnique({
where: { id: proposal_id },
select: { process_id: true }
});

if (!proposal) {
throw new Error('Proposal not found');
}

await prisma.participation.upsert({
where: {
user_id_process_id_participation_type: {
user_id: Number(session.user.id),
process_id: proposal.process_id,
participation_type: 'comment',
},
},
update: {},
create: {
user: {
connect: { id: Number(session.user.id) }
},
process: {
connect: { id: proposal.process_id }
},
participation_type: 'comment',
},
});


return NextResponse.json(
{ message: 'Comment submitted successfully', comment: new_comment.id, process_id: proposal.process_id},
{ status: 201 }
);
} catch (error) {
console.error('Comment error:', error);
return NextResponse.json(
{ error: 'Internal server error', fieldErrors: {} },
{ status: 500 }
);
}
}
Loading