Skip to content

Commit 64cee93

Browse files
Merge pull request #14 from ChangePlusPlusVandy/setup/dev-setup
Setup/dev setup
2 parents 8cd263b + 0c40baa commit 64cee93

File tree

13 files changed

+422
-1290
lines changed

13 files changed

+422
-1290
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ npx prisma generate
8282

8383
## Notes
8484

85-
Backend uses PostgreSQL/Supabase, Prisma ORM, Firebase Auth, Stripe, SendGrid, and AWS S3. Before each push, run `npm run format` on both frontend and backend directories for code consistency for all devs
85+
Backend uses PostgreSQL/Supabase, Prisma ORM, Clerk Auth, Stripe, AWS SES, and AWS S3. Before each push, run `npm run format` on both frontend and backend directories for code consistency for all devs. Strapi will be implemented later for total customization on admin side of user content
8686

8787
**Documentation Links For Reference:**
8888
- [PostgreSQL Docs](https://www.postgresql.org/docs/)
8989
- [Prisma Docs](https://www.prisma.io/docs/)
9090
- [Supabase Docs](https://supabase.com/docs)
9191
- [AWS S3 Docs](https://docs.aws.amazon.com/s3/)
92-
- [Firebase Auth Docs](https://firebase.google.com/docs/auth)
92+
- [Clerk Auth Docs](https://clerk.com/docs/index)
93+
- [AWS SES Docs](https://docs.aws.amazon.com/ses/)
9394
- [Stripe API Docs](https://stripe.com/docs/api)
94-
- [SendGrid API Docs](https://docs.sendgrid.com/)
9595
- [Docker Desktop Docs](https://docs.docker.com/desktop/)
96+
- [Strapi CMS Docs](https://docs.strapi.io/cms/quick-start)

backend/__tests__/controllers/organizationController.test.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
PrismaClient,
77
} from '@prisma/client';
88
import { mockDeep, mockReset } from 'jest-mock-extended';
9-
import admin from 'firebase-admin';
109

1110
const prismaMock = mockDeep<PrismaClient>();
1211

@@ -37,17 +36,21 @@ jest.mock('@prisma/client', () => ({
3736
},
3837
}));
3938

40-
jest.mock('firebase-admin', () => ({
41-
auth: jest.fn().mockReturnValue({
39+
const mockClerkClient = {
40+
users: {
4241
createUser: jest.fn().mockResolvedValue({
43-
uid: 'firebase-uid-123',
44-
email: 'test@nonprofit.org',
42+
id: 'clerk-user-123',
43+
emailAddresses: [{ emailAddress: 'test@nonprofit.org' }],
4544
}),
4645
updateUser: jest.fn().mockResolvedValue({
47-
uid: 'firebase-uid-123',
46+
id: 'clerk-user-123',
4847
}),
49-
verifyIdToken: jest.fn(),
50-
}),
48+
},
49+
verifyToken: jest.fn(),
50+
};
51+
52+
jest.mock('../../config/clerk', () => ({
53+
clerkClient: mockClerkClient,
5154
}));
5255

5356
jest.mock('../../config/prisma', () => ({
@@ -79,7 +82,7 @@ const createMockResponse = (): any => {
7982

8083
const mockOrganization = {
8184
id: 'org123',
82-
firebaseUid: 'firebase-uid-123',
85+
clerkId: 'clerk-user-123',
8386
email: 'contact@nonprofitorg.org',
8487
name: 'Community Senior Services',
8588
description: 'Providing services to seniors in the Nashville area',
@@ -109,19 +112,18 @@ const mockAdminOrg = {
109112
role: 'ADMIN' as OrganizationRole,
110113
name: 'Tennessee Coalition for Better Aging',
111114
email: 'admin@tcba.org',
112-
firebaseUid: 'firebase-admin-123',
115+
clerkId: 'clerk-admin-123',
113116
};
114117

115118
describe('OrganizationController', () => {
116119
beforeEach(() => {
117120
jest.clearAllMocks();
118121
mockReset(prismaMock);
119-
const mockAuth = admin.auth() as jest.Mocked<any>;
120-
mockAuth.updateUser.mockResolvedValue({ uid: 'firebase-uid-123' });
121-
mockAuth.createUser.mockResolvedValue({
122-
uid: 'firebase-uid-123',
123-
email: 'test@nonprofit.org',
124-
});
122+
mockClerkClient.users.updateUser.mockResolvedValue({ id: 'clerk-user-123' } as any);
123+
mockClerkClient.users.createUser.mockResolvedValue({
124+
id: 'clerk-user-123',
125+
emailAddresses: [{ emailAddress: 'test@nonprofit.org' }],
126+
} as any);
125127
});
126128

127129
describe('getAllOrganizations', () => {
@@ -177,11 +179,12 @@ describe('OrganizationController', () => {
177179
OR: [{ email: 'neworg@nonprofit.org' }, { name: 'New Community Services' }],
178180
},
179181
});
180-
expect(admin.auth().createUser).toHaveBeenCalledWith({
181-
email: 'neworg@nonprofit.org',
182+
expect(mockClerkClient.users.createUser).toHaveBeenCalledWith({
183+
emailAddress: ['neworg@nonprofit.org'],
182184
password: 'securePassword123',
183-
emailVerified: false,
184-
displayName: 'New Community Services',
185+
firstName: 'New',
186+
lastName: 'Community Services',
187+
publicMetadata: { organizationName: 'New Community Services', role: 'MEMBER' },
185188
});
186189
expect(prismaMock.organization.create).toHaveBeenCalledWith({
187190
data: {
@@ -205,7 +208,7 @@ describe('OrganizationController', () => {
205208
membershipRenewalDate: null,
206209
organizationSize: 'SMALL',
207210
tags: ['Community Services', 'Education'],
208-
firebaseUid: 'firebase-uid-123',
211+
clerkId: 'clerk-user-123',
209212
role: 'MEMBER',
210213
status: 'PENDING',
211214
},
@@ -375,7 +378,7 @@ describe('OrganizationController', () => {
375378
const res = createMockResponse();
376379
prismaMock.organization.findFirst.mockResolvedValue(null);
377380
prismaMock.organization.findUnique.mockResolvedValueOnce({
378-
firebaseUid: 'firebase-uid-123',
381+
clerkId: 'clerk-user-123',
379382
} as any);
380383
const updatedOrg = {
381384
...mockOrganization,
@@ -389,8 +392,8 @@ describe('OrganizationController', () => {
389392
NOT: { id: 'org123' },
390393
},
391394
});
392-
expect(admin.auth().updateUser).toHaveBeenCalledWith('firebase-uid-123', {
393-
email: 'newemail@nonprofit.org',
395+
expect(mockClerkClient.users.updateUser).toHaveBeenCalledWith('clerk-user-123', {
396+
externalId: 'newemail@nonprofit.org',
394397
});
395398
expect(prismaMock.organization.update).toHaveBeenCalledWith({
396399
where: { id: 'org123' },

backend/config/clerk.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createClerkClient } from '@clerk/express';
2+
3+
if (!process.env.CLERK_SECRET_KEY) {
4+
throw new Error('CLERK_SECRET_KEY environment variable is required');
5+
}
6+
7+
export const clerkClient = createClerkClient({
8+
secretKey: process.env.CLERK_SECRET_KEY,
9+
});

backend/controllers/organizationController.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
OrganizationSize,
77
} from '@prisma/client';
88
import { AuthenticatedRequest } from '../types/index.js';
9-
import admin from 'firebase-admin';
9+
import { clerkClient } from '../config/clerk.js';
1010
import { prisma } from '../config/prisma.js';
1111

1212
/**
@@ -119,12 +119,18 @@ export const registerOrganization = async (req: Request, res: Response) => {
119119
if (existingOrg) {
120120
return res.status(400).json({ error: 'Organization with this email or name already exists' });
121121
}
122-
const firebaseUser = await admin.auth().createUser({
123-
email,
122+
123+
const clerkUser = await clerkClient.users.createUser({
124+
emailAddress: [email],
124125
password,
125-
emailVerified: false,
126-
displayName: name,
126+
firstName: name.split(' ')[0] || name,
127+
lastName: name.split(' ').slice(1).join(' ') || '',
128+
publicMetadata: {
129+
organizationName: name,
130+
role: 'MEMBER',
131+
},
127132
});
133+
128134
let organizationTags = [];
129135
if (tags) {
130136
organizationTags = tags;
@@ -151,7 +157,7 @@ export const registerOrganization = async (req: Request, res: Response) => {
151157
membershipRenewalDate: membershipRenewalDate ? new Date(membershipRenewalDate) : null,
152158
organizationSize,
153159
tags: organizationTags,
154-
firebaseUid: firebaseUser.uid,
160+
clerkId: clerkUser.id,
155161
role: 'MEMBER',
156162
status: 'PENDING',
157163
},
@@ -298,14 +304,14 @@ export const updateOrganization = async (req: AuthenticatedRequest, res: Respons
298304
return res.status(404).json({ error: 'Organization not found' });
299305
}
300306
try {
301-
await admin.auth().updateUser(currentOrg.firebaseUid, {
302-
email: email,
307+
await clerkClient.users.updateUser(currentOrg.clerkId, {
308+
externalId: email,
303309
});
304-
} catch (firebaseError: any) {
305-
console.error('Firebase email update failed:', firebaseError);
310+
} catch (clerkError: any) {
311+
console.error('Clerk email update failed:', clerkError);
306312
return res.status(400).json({
307313
error: 'Failed to update email in authentication system',
308-
details: firebaseError.message,
314+
details: clerkError.message,
309315
});
310316
}
311317
updateData.email = email;

backend/middleware/auth.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Request, Response, NextFunction } from 'express';
2-
import { auth } from '../config/firebase';
2+
import { clerkClient } from '../config/clerk.js';
33

4-
interface AuthRequest extends Request {
4+
export interface AuthRequest extends Request {
5+
auth?: {
6+
userId: string;
7+
sessionId?: string;
8+
};
59
user?: {
6-
[key: string]: any;
7-
uid: string;
10+
id: string;
11+
clerkId: string;
812
email: string;
13+
role?: string;
914
};
1015
}
1116

@@ -23,15 +28,16 @@ export const authenticateToken = async (
2328
return;
2429
}
2530

26-
const decodedToken = await auth.verifyIdToken(token);
27-
28-
req.user = {
29-
uid: decodedToken.uid,
30-
email: decodedToken.email ?? '',
31-
};
32-
3331
next();
3432
} catch (error) {
3533
res.status(403).json({ error: 'Invalid token' });
3634
}
3735
};
36+
37+
export const requireAdmin = async (
38+
req: AuthRequest,
39+
res: Response,
40+
next: NextFunction
41+
): Promise<void> => {
42+
next();
43+
};

0 commit comments

Comments
 (0)