Skip to content

Commit 267f89b

Browse files
Merge pull request #69 from StreetSupport/staging
Merge staging into main
2 parents 65fbb9a + f31803a commit 267f89b

File tree

8 files changed

+232
-38
lines changed

8 files changed

+232
-38
lines changed

README.md

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# Street Support Platform API
2+
3+
This is the **Backend API** for the Street Support Network platform, providing data services for the Admin CMS and Public Website.
4+
5+
---
6+
7+
## 🚀 Tech Stack
8+
9+
- **Express.js** with TypeScript
10+
- **MongoDB** with Mongoose ODM
11+
- **Auth0** for JWT authentication
12+
- **Azure Blob Storage** for file uploads
13+
- **SendGrid** for transactional emails
14+
- **Zod** for runtime validation
15+
- **node-cron** for background jobs
16+
- **Jest** for testing
17+
18+
---
19+
20+
## 🧪 Testing
21+
22+
Unit tests use **Jest**.
23+
24+
Run all tests:
25+
```bash
26+
npm run test
27+
```
28+
29+
Run tests in watch mode:
30+
```bash
31+
npm run test:watch
32+
```
33+
34+
**Testing Highlights:**
35+
- Tests located in `tests/` directory
36+
- Smoke test currently configured
37+
- Full test suite to be implemented
38+
39+
**All tests must pass before merging into `staging` or `main`.**
40+
41+
---
42+
43+
## 🧹 Linting
44+
45+
ESLint is configured for code quality:
46+
47+
```bash
48+
# Run linting
49+
npm run lint
50+
51+
# Fix auto-fixable issues
52+
npm run lint:fix
53+
```
54+
55+
---
56+
57+
## 📂 Local Development
58+
59+
Run the project locally:
60+
61+
```bash
62+
npm install
63+
npm run dev
64+
```
65+
66+
### Required Environment Variables
67+
68+
Create a `.env` file with:
69+
70+
```bash
71+
# Server
72+
PORT=5000
73+
NODE_ENV=development
74+
75+
# MongoDB
76+
MONGODB_URI=mongodb+srv://...
77+
78+
# Auth0
79+
AUTH0_DOMAIN='take it from https://manage.auth0.com/. For example: your-tenant.auth0.com'
80+
AUTH0_AUDIENCE='take it from https://manage.auth0.com/'
81+
AUTH0_USER_DB_CONNECTION='take it from https://manage.auth0.com/'
82+
AUTH0_MANAGEMENT_CLIENT_ID='take it from https://manage.auth0.com/'
83+
AUTH0_MANAGEMENT_CLIENT_SECRET='take it from https://manage.auth0.com/'
84+
AUTH0_MANAGEMENT_AUDIENCE='take it from https://manage.auth0.com/. For example: https://your-tenant.auth0.com/api/v2/'
85+
86+
# Azure Blob Storage
87+
AZURE_STORAGE_CONNECTION_STRING=DefaultEndpointsProtocol=https;...
88+
AZURE_BANNERS_CONTAINER_NAME=banners
89+
AZURE_SWEPS_CONTAINER_NAME=sweps
90+
AZURE_RESOURCES_CONTAINER_NAME=resources
91+
AZURE_LOCATION_LOGOS_CONTAINER_NAME=location-logos
92+
93+
# SendGrid
94+
SENDGRID_API_KEY=SG...
95+
FROM_EMAIL=
96+
SENDGRID_ORG_UPDATE_NOTIFICATION_REMINDER_TEMPLATE_ID=d-...
97+
SENDGRID_ORG_VERIFICATION_EXPIRED_NOTIFICATION_TEMPLATE_ID=d-...
98+
ADMIN_URL=https://admin.streetsupport.net
99+
100+
# Sentry (optional)
101+
SENTRY_DSN=https://[email protected]/...
102+
```
103+
104+
---
105+
106+
## 🧭 Project Structure
107+
108+
```
109+
src/
110+
├── app.ts # Express app configuration
111+
├── index.ts # Server entry point
112+
├── config/ # Configuration (Auth0, etc.)
113+
├── constants/ # Role definitions, HTTP methods
114+
├── controllers/ # Request handlers
115+
├── jobs/ # Cron background jobs
116+
├── middleware/ # Auth, upload middleware
117+
├── models/ # Mongoose models
118+
├── routes/ # Express routes
119+
├── schemas/ # Zod validation schemas
120+
├── services/ # Business logic (email, Auth0)
121+
├── types/ # TypeScript interfaces
122+
└── utils/ # Helper utilities
123+
tests/
124+
└── smoke.test.js # Smoke tests
125+
```
126+
127+
---
128+
129+
## 🔐 Authentication
130+
131+
The API uses **Auth0 JWT tokens** for authentication:
132+
133+
1. Admin panel authenticates users via Auth0
134+
2. API receives Bearer token in Authorization header
135+
3. Middleware validates token and loads user from MongoDB
136+
4. RBAC middleware checks user roles for endpoint access
137+
138+
### Roles
139+
140+
| Role | Description |
141+
|------|-------------|
142+
| `SuperAdmin` | Full platform access |
143+
| `CityAdmin` | Location-specific access |
144+
| `VolunteerAdmin` | Organisation management |
145+
| `OrgAdmin` | Single organisation access |
146+
| `SwepAdmin` | SWEP banner management |
147+
148+
---
149+
150+
## 📡 API Endpoints
151+
152+
### Core Resources
153+
154+
| Resource | Endpoints | Auth |
155+
|----------|-----------|------|
156+
| **Users** | `/api/users` | Admin roles |
157+
| **Organisations** | `/api/organisations` | Role-based |
158+
| **Services** | `/api/services` | Role-based |
159+
| **Accommodations** | `/api/accommodations` | Role-based |
160+
| **Banners** | `/api/banners` | Role-based |
161+
| **SWEP Banners** | `/api/swep-banners` | SWEP/City Admin |
162+
| **FAQs** | `/api/faqs` | Role-based |
163+
| **Cities** | `/api/cities` | Authenticated |
164+
| **Location Logos** | `/api/location-logos` | City Admin |
165+
| **Resources** | `/api/resources` | Volunteer Admin |
166+
| **Service Categories** | `/api/service-categories` | Public read |
167+
168+
---
169+
170+
## ⏰ Background Jobs
171+
172+
| Job | Schedule | Purpose |
173+
|-----|----------|---------|
174+
| Verification Check | Daily 9 AM | Send reminders, unverify stale organisations |
175+
| Banner Activation | Daily 00:05 AM | Activate/deactivate scheduled banners |
176+
| SWEP Activation | Daily 00:00 AM | Track SWEP banner activation times |
177+
| Organisation Disabling | Daily 00:10 AM | Handle extended inactivity |
178+
179+
---
180+
181+
## 🔄 Deployment
182+
183+
### Environments
184+
185+
| Environment | Branch | Azure Service |
186+
|-------------|--------|---------------|
187+
| Staging | `staging` | `streetsupport-api-staging` |
188+
| Production | `main` | `streetsupport-api` |
189+
190+
### CI/CD
191+
192+
1. Create feature branch from `staging`
193+
2. PR triggers tests and linting
194+
3. Merge to `staging` → Deploy to staging
195+
4. Merge to `main` → Deploy to production
196+
197+
---
198+
199+
## 📚 Documentation
200+
201+
Comprehensive documentation is available in the Admin project:
202+
- **[Admin Project Docs](../streetsupport-platform-admin/docs/)**
203+
- [Permissions System](../streetsupport-platform-admin/docs/PERMISSIONS.md)
204+
- [Collection Schemas](../streetsupport-platform-admin/docs/COLLECTION_SCHEMAS.md)
205+
- [Validation (Zod)](../streetsupport-platform-admin/docs/VALIDATION.md)
206+
- [File Uploading](../streetsupport-platform-admin/docs/FILE_UPLOADING.md)
207+
- [Cron Jobs & SendGrid](../streetsupport-platform-admin/docs/CRON_JOBS_AND_SENDGRID.md)
208+
209+
---
210+
211+
## 🔗 Related Projects
212+
213+
- **Admin CMS**: [streetsupport-platform-admin](../streetsupport-platform-admin)
214+
- **Public Website**: [streetsupport-platform-web](../streetsupport-platform-web)
215+
216+
---
217+
218+
## 📝 Contribution Guidelines
219+
220+
1. Create feature branch from `staging`
221+
2. Follow existing code patterns
222+
3. Add appropriate tests
223+
4. Ensure linting passes
224+
5. Create PR with description
225+
6. Wait for review and CI checks
226+
227+
---
228+
229+
## License
230+
231+
This project is licensed under the [MIT License](LICENSE).

src/controllers/bannerController.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -312,26 +312,6 @@ export const toggleBannerStatus = asyncHandler(async (req: Request, res: Respons
312312
return sendSuccess(res, updatedBanner, `Banner ${updatedBanner?.IsActive ? 'activated' : 'deactivated'} successfully`);
313313
});
314314

315-
// TODO: Remove it if we are going to get "Downloads" from GA4
316-
// Increment download count for resource banners
317-
export const incrementDownloadCount = asyncHandler(async (req: Request, res: Response) => {
318-
const { id } = req.params;
319-
320-
const banner = await Banner.findById(id);
321-
322-
if (!banner) {
323-
return sendNotFound(res, 'Banner not found');
324-
}
325-
326-
if (banner.TemplateType !== BannerTemplateType.RESOURCE_PROJECT) {
327-
return sendBadRequest(res, 'Download count can only be incremented for resource project banners');
328-
}
329-
330-
await banner.IncrementDownloadCount();
331-
332-
return sendSuccess(res, { DownloadCount: banner.ResourceProject?.ResourceFile?.DownloadCount || 0 }, 'Download count incremented');
333-
});
334-
335315
// Private helper to handle resource project specific logic
336316
function _handleResourceProjectBannerLogic(bannerData: any): any {
337317
if (bannerData.TemplateType === BannerTemplateType.RESOURCE_PROJECT && bannerData.ResourceProject?.ResourceFile) {

src/models/bannerModel.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,6 @@ BannerSchema.index({ LocationSlug: 1, IsActive: 1 });
111111
BannerSchema.index({ TemplateType: 1, IsActive: 1 });
112112
BannerSchema.index({ CreatedBy: 1 });
113113

114-
// Instance methods
115-
BannerSchema.methods.IncrementDownloadCount = function() {
116-
if (this.TemplateType === BannerTemplateType.RESOURCE_PROJECT && this.ResourceProject?.ResourceFile) {
117-
this.ResourceProject.ResourceFile.DownloadCount = (this.ResourceProject.ResourceFile.DownloadCount || 0) + 1;
118-
return this.save();
119-
}
120-
};
121-
122114
// Create and export the model
123115
export const Banner = mongoose.model<IBanner>('Banners', BannerSchema);
124116

src/models/organisationModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ const organisationSchema = new Schema<IOrganisation>({
9191
// Note: Key field already has unique: true in schema definition, no need for separate index
9292
organisationSchema.index({ Name: 1 });
9393
organisationSchema.index({ IsPublished: 1, AssociatedLocationIds: 1 });
94+
// It was created in MongoDb manually
9495
// organisationSchema.index({ Key: 1 }, { unique: true }); // Unique index to prevent duplicate keys
9596
organisationSchema.index({ AssociatedLocationIds: 1, Name: 1 });
9697
organisationSchema.index({ IsPublished: 1, DocumentCreationDate: -1 });

src/routes/bannerRoutes.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,12 @@ import {
66
updateBanner,
77
deleteBanner,
88
toggleBannerStatus,
9-
incrementDownloadCount
109
} from '../controllers/bannerController.js';
1110
import { bannersAuth, bannersByLocationAuth } from '../middleware/authMiddleware.js';
1211
import { bannersUploadMiddleware } from '../middleware/uploadMiddleware.js';
1312

1413
const router = Router();
1514

16-
// Public routes
17-
router.post('/:id/download', incrementDownloadCount);
18-
1915
// Protected routes
2016
router.get('/', bannersByLocationAuth, getBanners);
2117
router.get('/:id', bannersAuth, getBannerById);

src/schemas/bannerSchemaCore.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@ export const ResourceFileSchemaCore = z.object({
6767
FileUrl: z.string().min(1, 'File URL is required'),
6868
FileName: z.string().min(1, 'File name is required'),
6969
ResourceType: z.nativeEnum(ResourceType).default(ResourceType.GUIDE),
70-
DownloadCount: z.number().min(0).optional().default(0),
7170
LastUpdated: z.date().default(() => new Date()),
7271
FileSize: z.string().min(1, 'File size is required').max(20, 'File size must be 20 characters or less'),
7372
FileType: z.string().min(1, 'File type is required').max(10, 'File type must be 10 characters or less')

src/types/banners/IBanner.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ export interface IBanner extends Document {
6464
LocationName?: string;
6565
Priority: number;
6666
TrackingContext?: string;
67-
68-
// Methods
69-
IncrementDownloadCount(): Promise<IBanner>;
7067
}
7168

7269
// Enums for type safety

src/types/banners/IResourceFile.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export interface IResourceFile {
55
FileUrl: string;
66
FileName: string;
77
ResourceType: ResourceType;
8-
DownloadCount?: number;
98
LastUpdated: Date;
109
FileSize: string;
1110
FileType: string;
@@ -15,7 +14,6 @@ export const ResourceFileSchema = new Schema<IResourceFile>({
1514
FileUrl: { type: String, required: true },
1615
FileName: { type: String, required: true},
1716
ResourceType: { type: String, enum: Object.values(ResourceType), required: true },
18-
DownloadCount: { type: Number, min: 0, default: 0 },
1917
LastUpdated: { type: Date, required: true },
2018
FileSize: { type: String, required: true },
2119
FileType: { type: String, required: true }

0 commit comments

Comments
 (0)