Skip to content

Commit 38a97a0

Browse files
committed
Start implementing logos
1 parent 8702946 commit 38a97a0

File tree

14 files changed

+1821
-4
lines changed

14 files changed

+1821
-4
lines changed

src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import userRoutes from './routes/userRoutes.js';
99
import bannerRoutes from './routes/bannerRoutes.js';
1010
import swepBannerRoutes from './routes/swepBannerRoutes.js';
1111
import resourceRoutes from './routes/resourceRoutes.js';
12+
import locationLogoRoutes from './routes/locationLogoRoutes.js';
1213
import { errorHandler, notFound } from './middleware/errorMiddleware.js';
1314
import checkJwt from './middleware/checkJwt.js';
1415
import './instrument.js';
@@ -38,6 +39,7 @@ app.use('/api/users', userRoutes);
3839
app.use('/api/banners', bannerRoutes);
3940
app.use('/api/swep-banners', swepBannerRoutes);
4041
app.use('/api/resources', resourceRoutes);
42+
app.use('/api/location-logos', locationLogoRoutes);
4143

4244
// The error handler must be registered before any other error middleware and after all controllers
4345
Sentry.setupExpressErrorHandler(app);

src/controllers/bannerController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const getBanners = asyncHandler(async (req: Request, res: Response) => {
1616
isActive,
1717
search,
1818
page = 1,
19-
limit = 10,
19+
limit = 9,
2020
sortBy = 'Priority',
2121
sortOrder = 'desc'
2222
} = req.query;

src/controllers/faqController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const getFaqs = asyncHandler(async (req: Request, res: Response) => {
1313
location,
1414
search,
1515
page = 1,
16-
limit = 10,
16+
limit = 9,
1717
sortBy = 'DocumentModifiedDate',
1818
sortOrder = 'desc'
1919
} = req.query;
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { Request, Response } from 'express';
2+
import { asyncHandler } from '../utils/asyncHandler.js';
3+
import { sendSuccess, sendBadRequest, sendNotFound, sendPaginatedSuccess } from '../utils/apiResponses.js';
4+
import { validateLocationLogo } from '../schemas/locationLogoSchema.js';
5+
import LocationLogo from '../models/locationLogosModel.js';
6+
import { deleteFile } from '../middleware/uploadMiddleware.js';
7+
8+
// @desc Get all location logos with optional filtering
9+
// @route GET /api/location-logos
10+
// @access Private
11+
export const getLocationLogos = asyncHandler(async (req: Request, res: Response) => {
12+
const {
13+
location,
14+
locations, // Comma-separated list for location filtering
15+
search,
16+
page = 1,
17+
limit = 9,
18+
sortBy = 'DocumentModifiedDate',
19+
sortOrder = 'desc'
20+
} = req.query;
21+
22+
const query: any = {};
23+
const conditions: any[] = [];
24+
25+
// Apply search filter
26+
if (search && typeof search === 'string') {
27+
conditions.push({
28+
$or: [
29+
{ DisplayName: { $regex: search.trim(), $options: 'i' } }
30+
]
31+
});
32+
}
33+
34+
// Apply location filter
35+
if (locations && typeof locations === 'string') {
36+
const locationArray = locations.split(',').map(loc => loc.trim()).filter(Boolean);
37+
if (locationArray.length > 0) {
38+
conditions.push({ LocationSlug: { $in: locationArray } });
39+
}
40+
} else if (location && typeof location === 'string') {
41+
conditions.push({ LocationSlug: location });
42+
}
43+
44+
// Combine all conditions with AND logic
45+
if (conditions.length > 0) {
46+
query.$and = conditions;
47+
}
48+
49+
// Pagination
50+
const skip = (Number(page) - 1) * Number(limit);
51+
52+
// Sort options
53+
const sortOptions: any = {};
54+
sortOptions[sortBy as string] = sortOrder === 'desc' ? -1 : 1;
55+
56+
const logos = await LocationLogo.find(query)
57+
.sort(sortOptions)
58+
.skip(skip)
59+
.limit(Number(limit))
60+
.lean();
61+
62+
// Get total count using the same query
63+
const total = await LocationLogo.countDocuments(query);
64+
65+
return sendPaginatedSuccess(res, logos, {
66+
page: Number(page),
67+
limit: Number(limit),
68+
total,
69+
pages: Math.ceil(total / Number(limit))
70+
});
71+
});
72+
73+
// @desc Get single location logo by ID
74+
// @route GET /api/location-logos/:id
75+
// @access Private
76+
export const getLocationLogoById = asyncHandler(async (req: Request, res: Response) => {
77+
const logo = await LocationLogo.findById(req.params.id);
78+
79+
if (!logo) {
80+
return sendNotFound(res, 'Location logo not found');
81+
}
82+
83+
return sendSuccess(res, logo);
84+
});
85+
86+
87+
// @desc Create location logo
88+
// @route POST /api/location-logos
89+
// @access Private
90+
export const createLocationLogo = asyncHandler(async (req: Request, res: Response) => {
91+
// Extract uploaded file URL from req
92+
let logoPath = '';
93+
if (req.body.LogoPath) {
94+
logoPath = req.body.LogoPath;
95+
}
96+
97+
const logoData = {
98+
...req.body,
99+
LogoPath: logoPath,
100+
CreatedBy: req.user?.id || 'system'
101+
};
102+
103+
// Validate logo data
104+
const validation = validateLocationLogo(logoData);
105+
if (!validation.success) {
106+
return sendBadRequest(res, 'Validation failed');
107+
}
108+
109+
// Create location logo
110+
const logo = await LocationLogo.create(logoData);
111+
112+
return sendSuccess(res, logo, 'Location logo created successfully');
113+
});
114+
115+
// @desc Update location logo
116+
// @route PUT /api/location-logos/:id
117+
// @access Private
118+
export const updateLocationLogo = asyncHandler(async (req: Request, res: Response) => {
119+
const { id } = req.params;
120+
121+
// Get existing location logo
122+
const existingLogo = await LocationLogo.findById(id);
123+
124+
if (!existingLogo) {
125+
return sendNotFound(res, 'Location logo not found');
126+
}
127+
128+
// Store old logo path for cleanup
129+
const oldLogoPath = existingLogo.LogoPath;
130+
131+
// Extract uploaded file URL from req
132+
let logoPath = existingLogo.LogoPath;
133+
if (req.body.LogoPath && req.body.LogoPath !== existingLogo.LogoPath) {
134+
logoPath = req.body.LogoPath;
135+
}
136+
137+
const logoData = {
138+
...req.body,
139+
LogoPath: logoPath,
140+
DocumentModifiedDate: new Date()
141+
};
142+
143+
// Validate logo data
144+
const validation = validateLocationLogo(logoData);
145+
if (!validation.success) {
146+
return sendBadRequest(res, 'Validation failed');
147+
}
148+
149+
// Update location logo
150+
const updatedLogo = await LocationLogo.findByIdAndUpdate(
151+
id,
152+
logoData,
153+
{ new: true, runValidators: true }
154+
);
155+
156+
// Cleanup old logo file if it was replaced
157+
if (logoPath !== oldLogoPath && oldLogoPath) {
158+
try {
159+
await deleteFile(oldLogoPath);
160+
console.log(`Deleted old logo file: ${oldLogoPath}`);
161+
} catch (error) {
162+
console.error(`Failed to delete old logo file: ${oldLogoPath}`, error);
163+
}
164+
}
165+
166+
return sendSuccess(res, updatedLogo, 'Location logo updated successfully');
167+
});
168+
169+
// @desc Delete location logo
170+
// @route DELETE /api/location-logos/:id
171+
// @access Private
172+
export const deleteLocationLogo = asyncHandler(async (req: Request, res: Response) => {
173+
const { id } = req.params;
174+
175+
const logo = await LocationLogo.findById(id);
176+
177+
if (!logo) {
178+
return sendNotFound(res, 'Location logo not found');
179+
}
180+
181+
// Store logo path for cleanup
182+
const logoPath = logo.LogoPath;
183+
184+
// Delete from database
185+
await LocationLogo.findByIdAndDelete(id);
186+
187+
// Delete logo file from storage
188+
if (logoPath) {
189+
try {
190+
await deleteFile(logoPath);
191+
console.log(`Deleted logo file: ${logoPath}`);
192+
} catch (error) {
193+
console.error(`Failed to delete logo file: ${logoPath}`, error);
194+
}
195+
}
196+
197+
return sendSuccess(res, null, 'Location logo deleted successfully');
198+
});

src/controllers/swepBannerController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const getSwepBanners = asyncHandler(async (req: Request, res: Response) =
1515
isActive,
1616
search,
1717
page = 1,
18-
limit = 10,
18+
limit = 9,
1919
sortBy = 'DocumentModifiedDate',
2020
sortOrder = 'desc'
2121
} = req.query;

src/controllers/userController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const getUsers = asyncHandler(async (req: Request, res: Response) => {
1919
location,
2020
locations, // New: comma-separated list of locations for CityAdmin filtering
2121
page = 1,
22-
limit = 10,
22+
limit = 9,
2323
sortBy = 'DocumentModifiedDate',
2424
sortOrder = 'desc'
2525
} = req.query;

src/middleware/authMiddleware.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { asyncHandler } from '../utils/asyncHandler.js';
2020
import Accommodation from '../models/accommodationModel.js';
2121
import GroupedService from '../models/groupedServiceModel.js';
2222
import SwepBanner from '../models/swepModel.js';
23+
import LocationLogo from '../models/locationLogosModel.js';
2324

2425
type PreValidatedBannerData = z.output<typeof BannerPreUploadApiSchema>;
2526
// Extend Request interface to include user
@@ -1587,3 +1588,104 @@ export const resourcesAuth = [
15871588
authenticate,
15881589
requireResourceAccess
15891590
];
1591+
1592+
/**
1593+
* Middleware for location logo access control with location validation
1594+
*/
1595+
export const requireLocationLogoAccess = asyncHandler(async (req: Request, res: Response, next: NextFunction) => {
1596+
if (ensureAuthenticated(req, res)) return;
1597+
1598+
if (req.method === HTTP_METHODS.GET || req.method === HTTP_METHODS.PUT || req.method === HTTP_METHODS.POST || req.method === HTTP_METHODS.DELETE) {
1599+
const userAuthClaims = req.user?.AuthClaims || [];
1600+
1601+
// SuperAdmin and VolunteerAdmin global rule
1602+
if (handleSuperAdminAccess(userAuthClaims) || handleVolunteerAdminAccess(userAuthClaims)) { return next(); }
1603+
1604+
// Check if user has CityAdmin role
1605+
if (!userAuthClaims.includes(ROLES.CITY_ADMIN)) {
1606+
return sendForbidden(res);
1607+
}
1608+
1609+
// For operations on specific location logos, check LocationSlug access
1610+
const locationLogoIdOrSlug = req.params.id || req.body.LocationSlug;
1611+
1612+
if (locationLogoIdOrSlug) {
1613+
try {
1614+
// If it's an ID, get the logo first
1615+
let locationSlug = '';
1616+
if (req.params.id) {
1617+
const logo = await LocationLogo.findById(req.params.id).lean();
1618+
if (logo) {
1619+
locationSlug = logo.LocationSlug;
1620+
}
1621+
} else {
1622+
locationSlug = req.body.LocationSlug;
1623+
}
1624+
1625+
if (locationSlug) {
1626+
// Check if CityAdmin has access to this location
1627+
const locations = [locationSlug];
1628+
if (validateSwepAndCityAdminLocationsAccess(userAuthClaims, locations, res)) {
1629+
return; // Access denied, response already sent
1630+
}
1631+
}
1632+
1633+
next();
1634+
} catch (error) {
1635+
console.error('Error validating location logo access:', error);
1636+
return sendInternalError(res);
1637+
}
1638+
} else {
1639+
// No specific location, allow access for listing
1640+
next();
1641+
}
1642+
} else {
1643+
return sendForbidden(res, 'Invalid HTTP method for this endpoint');
1644+
}
1645+
});
1646+
1647+
/**
1648+
* Combined middleware for location logos endpoint
1649+
*/
1650+
export const locationLogosAuth = [
1651+
authenticate,
1652+
requireLocationLogoAccess
1653+
];
1654+
1655+
/**
1656+
* Middleware for location logos location-based access (GET list)
1657+
*/
1658+
export const requireLocationLogoByFiltersAccess = (req: Request, res: Response, next: NextFunction) => {
1659+
if (ensureAuthenticated(req, res)) return;
1660+
1661+
if (req.method !== HTTP_METHODS.GET) {
1662+
return sendForbidden(res, 'Invalid HTTP method for this endpoint');
1663+
}
1664+
1665+
const userAuthClaims = req.user?.AuthClaims || [];
1666+
1667+
// SuperAdmin and VolunteerAdmin global rule
1668+
if (handleSuperAdminAccess(userAuthClaims) || handleVolunteerAdminAccess(userAuthClaims)) { return next(); }
1669+
1670+
// Check if user has CityAdmin role
1671+
if (!userAuthClaims.includes(ROLES.CITY_ADMIN)) {
1672+
return sendForbidden(res);
1673+
}
1674+
1675+
// For location-based access, check the location and locations param
1676+
const locations = extractLocationsFromQuery(req);
1677+
1678+
if (validateSwepAndCityAdminLocationsAccess(userAuthClaims, locations, res)) {
1679+
return; // Access denied, response already sent
1680+
}
1681+
1682+
next();
1683+
};
1684+
1685+
/**
1686+
* Combined middleware for location logos GET list endpoint
1687+
*/
1688+
export const locationLogosGetAuth = [
1689+
authenticate,
1690+
requireLocationLogoByFiltersAccess
1691+
];

0 commit comments

Comments
 (0)