Skip to content

Commit 049b21b

Browse files
committed
add new features
1 parent e173f76 commit 049b21b

31 files changed

+1102
-9
lines changed

backend/src/app.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@ import morgan from 'morgan';
55
import { errorHandler, notFound } from '../src/middleware/error.middleware.js';
66

77
import authRoutes from './routes/auth.routes.js';
8+
import awardRoutes from './routes/award.routes.js';
9+
import categoryRoutes from './routes/category.routes.js';
10+
import clientProfileRoutes from './routes/clientProfile.routes.js';
11+
import developerProfileRoutes from './routes/developerProfile.routes.js';
12+
import matchRoutes from './routes/match.routes.js';
13+
import messageRoutes from './routes/message.routes.js';
14+
import notificationRoutes from './routes/notification.routes.js';
15+
import projectRoutes from './routes/project.routes.js';
16+
import projectRequestRoutes from './routes/projectRequest.routes.js';
817
import productRoutes from './routes/product.routes.js';
18+
import reviewRoutes from './routes/review.routes.js';
19+
import technologyRoutes from './routes/technology.routes.js';
920
import userRoutes from './routes/user.routes.js';
10-
// import orderRoutes from "./routes/order.routes.js";
11-
// import uploadRoutes from "./routes/upload.routes.js";
1221

1322
const app = express();
1423

@@ -29,12 +38,22 @@ app.use(
2938
);
3039

3140
app.use('/api/auth', authRoutes);
41+
app.use('/api/awards', awardRoutes);
42+
app.use('/api/categories', categoryRoutes);
43+
app.use('/api/client-profiles', clientProfileRoutes);
44+
app.use('/api/developer-profiles', developerProfileRoutes);
45+
app.use('/api/matches', matchRoutes);
46+
app.use('/api/messages', messageRoutes);
47+
app.use('/api/notifications', notificationRoutes);
48+
app.use('/api/projects', projectRoutes);
49+
app.use('/api/project-requests', projectRequestRoutes);
3250
app.use('/api/user', userRoutes);
3351
app.use('/api/product', productRoutes);
52+
app.use('/api/reviews', reviewRoutes);
53+
app.use('/api/technologies', technologyRoutes);
3454
// Legacy/Frontend-friendly routes
3555
app.use('/products', productRoutes);
36-
// app.use("/api/upload", uploadRoutes);
37-
// app.use("/api/order", orderRoutes);
56+
app.use('/projects', projectRoutes);
3857

3958
//test Route
4059
app.get('/', (req, res) => {

backend/src/controllers/auth.controller.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@ import User from '../models/user.model.js';
33
import generateToken from '../utils/generateToken.js';
44

55
// REGISTER (only for normal users, admin comes from seed file)
6+
const allowedRoles = new Set(["developer", "client"]);
7+
68
export const registerUser = async (req, res, next) => {
79
try {
8-
const { name, email, password } = req.body;
10+
const { name, email, password, role } = req.body;
911

1012
const userExists = await User.findOne({ email });
1113
if (userExists) {
1214
res.status(400);
1315
throw new Error('User already exists');
1416
}
1517

18+
const safeRole = allowedRoles.has(role) ? role : "client";
19+
1620
const user = await User.create({
1721
name,
1822
email,
1923
password,
20-
role: 'user', // Enforce only "user" role here
24+
role: safeRole,
2125
});
2226

2327
res.status(201).json({
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Award from '../models/award.model.js';
2+
import { buildCrudController } from './crud.controller.js';
3+
4+
const { list, getById, create, update, remove } = buildCrudController(Award, {
5+
resourceName: 'Award',
6+
searchFields: ['name', 'type'],
7+
filterFields: ['type', 'projectId', 'developerId', 'year'],
8+
allowedPopulate: ['projectId', 'developerId'],
9+
});
10+
11+
export const getAwards = list;
12+
export const getAwardById = getById;
13+
export const createAward = create;
14+
export const updateAward = update;
15+
export const deleteAward = remove;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Category from '../models/category.model.js';
2+
import { buildCrudController } from './crud.controller.js';
3+
4+
const { list, getById, create, update, remove } = buildCrudController(
5+
Category,
6+
{
7+
resourceName: 'Category',
8+
searchFields: ['name', 'description'],
9+
filterFields: ['name'],
10+
defaultSort: 'name',
11+
}
12+
);
13+
14+
export const getCategories = list;
15+
export const getCategoryById = getById;
16+
export const createCategory = create;
17+
export const updateCategory = update;
18+
export const deleteCategory = remove;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import ClientProfile from '../models/clientProfile.model.js';
2+
import { buildCrudController } from './crud.controller.js';
3+
4+
const { list, getById, create, update, remove } = buildCrudController(
5+
ClientProfile,
6+
{
7+
resourceName: 'Client profile',
8+
searchFields: ['companyName', 'industry', 'location', 'description'],
9+
filterFields: ['userId', 'industry', 'companySize', 'location'],
10+
ownerField: 'userId',
11+
assignOwnerOnCreate: true,
12+
}
13+
);
14+
15+
export const getClientProfiles = list;
16+
export const getClientProfileById = getById;
17+
export const createClientProfile = create;
18+
export const updateClientProfile = update;
19+
export const deleteClientProfile = remove;
20+
21+
export const getMyClientProfile = async (req, res, next) => {
22+
try {
23+
const profile = await ClientProfile.findOne({ userId: req.user._id });
24+
if (!profile) {
25+
return res.status(404).json({ message: 'Client profile not found' });
26+
}
27+
res.json(profile);
28+
} catch (error) {
29+
next(error);
30+
}
31+
};
32+
33+
export const upsertMyClientProfile = async (req, res, next) => {
34+
try {
35+
const payload = { ...req.body, userId: req.user._id };
36+
const profile = await ClientProfile.findOneAndUpdate(
37+
{ userId: req.user._id },
38+
payload,
39+
{
40+
new: true,
41+
upsert: true,
42+
runValidators: true,
43+
setDefaultsOnInsert: true,
44+
}
45+
);
46+
res.json(profile);
47+
} catch (error) {
48+
next(error);
49+
}
50+
};
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
const parsePositiveInt = (value) => {
2+
if (value === undefined || value === null || value === "") return null;
3+
const num = Number(value);
4+
if (!Number.isFinite(num) || num < 0) return null;
5+
return Math.floor(num);
6+
};
7+
8+
const escapeRegExp = (value) =>
9+
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10+
11+
const getPagination = (req) => {
12+
const start = parsePositiveInt(req.query._start);
13+
const limit = parsePositiveInt(req.query._limit);
14+
15+
if (start !== null || limit !== null) {
16+
return { skip: start || 0, limit: limit || 0 };
17+
}
18+
19+
const page = parsePositiveInt(req.query.page);
20+
const pageLimit = parsePositiveInt(req.query.limit);
21+
if (pageLimit !== null) {
22+
const safePage = page && page > 0 ? page : 1;
23+
return { skip: (safePage - 1) * pageLimit, limit: pageLimit };
24+
}
25+
26+
return { skip: 0, limit: 0 };
27+
};
28+
29+
const parseCommaList = (value) =>
30+
value
31+
.split(",")
32+
.map((item) => item.trim())
33+
.filter(Boolean);
34+
35+
const buildFilters = (query, allowedFields = []) => {
36+
const filters = {};
37+
for (const field of allowedFields) {
38+
const raw = query[field];
39+
if (raw === undefined) continue;
40+
if (typeof raw === "string" && raw.includes(",")) {
41+
filters[field] = { $in: parseCommaList(raw) };
42+
} else {
43+
filters[field] = raw;
44+
}
45+
}
46+
return filters;
47+
};
48+
49+
const buildSearchFilter = (searchTerm, searchFields = []) => {
50+
if (!searchTerm || !searchFields.length) return null;
51+
const regex = new RegExp(escapeRegExp(searchTerm), "i");
52+
return {
53+
$or: searchFields.map((field) => ({ [field]: regex })),
54+
};
55+
};
56+
57+
const applyPopulate = (query, req, allowedPopulate = [], defaultPopulate = []) => {
58+
const raw = req.query.populate;
59+
const requested = typeof raw === "string" ? parseCommaList(raw) : [];
60+
const populateFields = new Set([
61+
...defaultPopulate,
62+
...requested.filter((path) => allowedPopulate.includes(path)),
63+
]);
64+
for (const path of populateFields) {
65+
query = query.populate(path);
66+
}
67+
return query;
68+
};
69+
70+
const pickAllowed = (payload, allowedFields) => {
71+
if (!Array.isArray(allowedFields) || !allowedFields.length) {
72+
return { ...payload };
73+
}
74+
const picked = {};
75+
for (const field of allowedFields) {
76+
if (payload[field] !== undefined) {
77+
picked[field] = payload[field];
78+
}
79+
}
80+
return picked;
81+
};
82+
83+
const isAdmin = (req) => req.user && req.user.role === "admin";
84+
85+
const ensureOwner = (doc, req, ownerField) => {
86+
if (!ownerField || isAdmin(req)) return true;
87+
const ownerId = doc?.[ownerField]?.toString();
88+
const userId = req.user?._id?.toString();
89+
return ownerId && userId && ownerId === userId;
90+
};
91+
92+
const assignOwner = (payload, req, ownerField, allowAdminOverride = true) => {
93+
if (!ownerField || !req.user) return payload;
94+
if (!isAdmin(req) || !allowAdminOverride) {
95+
return { ...payload, [ownerField]: req.user._id };
96+
}
97+
if (!payload[ownerField]) {
98+
return { ...payload, [ownerField]: req.user._id };
99+
}
100+
return payload;
101+
};
102+
103+
const sanitizePayload = (payload, req, ownerField, allowedFields) => {
104+
const cleaned = pickAllowed(payload, allowedFields);
105+
if (ownerField && !isAdmin(req)) {
106+
delete cleaned[ownerField];
107+
}
108+
return cleaned;
109+
};
110+
111+
export const buildCrudController = (
112+
Model,
113+
{
114+
resourceName = "Resource",
115+
searchFields = [],
116+
filterFields = [],
117+
defaultSort = "-createdAt",
118+
allowedPopulate = [],
119+
defaultPopulate = [],
120+
ownerField = null,
121+
assignOwnerOnCreate = false,
122+
allowAdminOverride = true,
123+
allowedFields = null,
124+
scopeToUser = false,
125+
} = {}
126+
) => {
127+
const list = async (req, res, next) => {
128+
try {
129+
const { skip, limit } = getPagination(req);
130+
const filters = buildFilters(req.query, filterFields);
131+
if (scopeToUser && ownerField && req.user && !isAdmin(req)) {
132+
filters[ownerField] = req.user._id;
133+
}
134+
135+
const searchTerm = (req.query.q || req.query.search || "").trim();
136+
const searchFilter = buildSearchFilter(searchTerm, searchFields);
137+
138+
let query = Model.find(filters);
139+
if (searchFilter) {
140+
query = query.find(searchFilter);
141+
}
142+
143+
if (skip) query = query.skip(skip);
144+
if (limit) query = query.limit(limit);
145+
query = query.sort(req.query.sort || defaultSort);
146+
query = applyPopulate(query, req, allowedPopulate, defaultPopulate);
147+
148+
const docs = await query.exec();
149+
res.json(docs);
150+
} catch (error) {
151+
next(error);
152+
}
153+
};
154+
155+
const getById = async (req, res, next) => {
156+
try {
157+
let query = Model.findById(req.params.id);
158+
query = applyPopulate(query, req, allowedPopulate, defaultPopulate);
159+
const doc = await query.exec();
160+
if (!doc) {
161+
res.status(404);
162+
throw new Error(`${resourceName} not found`);
163+
}
164+
res.json(doc);
165+
} catch (error) {
166+
next(error);
167+
}
168+
};
169+
170+
const create = async (req, res, next) => {
171+
try {
172+
let payload = sanitizePayload(req.body || {}, req, ownerField, allowedFields);
173+
if (assignOwnerOnCreate) {
174+
payload = assignOwner(payload, req, ownerField, allowAdminOverride);
175+
}
176+
const created = await Model.create(payload);
177+
res.status(201).json(created);
178+
} catch (error) {
179+
next(error);
180+
}
181+
};
182+
183+
const update = async (req, res, next) => {
184+
try {
185+
const doc = await Model.findById(req.params.id);
186+
if (!doc) {
187+
res.status(404);
188+
throw new Error(`${resourceName} not found`);
189+
}
190+
if (!ensureOwner(doc, req, ownerField)) {
191+
res.status(403);
192+
throw new Error("Not authorized to update this resource");
193+
}
194+
const updates = sanitizePayload(req.body || {}, req, ownerField, allowedFields);
195+
Object.assign(doc, updates);
196+
const updated = await doc.save();
197+
res.json(updated);
198+
} catch (error) {
199+
next(error);
200+
}
201+
};
202+
203+
const remove = async (req, res, next) => {
204+
try {
205+
const doc = await Model.findById(req.params.id);
206+
if (!doc) {
207+
res.status(404);
208+
throw new Error(`${resourceName} not found`);
209+
}
210+
if (!ensureOwner(doc, req, ownerField)) {
211+
res.status(403);
212+
throw new Error("Not authorized to delete this resource");
213+
}
214+
await doc.deleteOne();
215+
res.json({ message: `${resourceName} deleted`, id: req.params.id });
216+
} catch (error) {
217+
next(error);
218+
}
219+
};
220+
221+
return { list, getById, create, update, remove };
222+
};
223+
224+
export {
225+
applyPopulate,
226+
buildFilters,
227+
buildSearchFilter,
228+
escapeRegExp,
229+
getPagination,
230+
parsePositiveInt,
231+
};

0 commit comments

Comments
 (0)