Compare commits
4 Commits
ccf1323849
...
0e4599343e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e4599343e | ||
|
|
1591f58eec | ||
|
|
d2e12ef23d | ||
|
|
cfc9b09a1f |
@@ -1,6 +1,6 @@
|
||||
const { body, validationResult } = require("express-validator");
|
||||
|
||||
const URL_REGEX = /^(https?|ftp):\\/\\/[^\\s\\/$.?#].[^\\s]*$/i;
|
||||
// URL validation handled via express-validator isURL
|
||||
|
||||
const validateProfile = [
|
||||
body("bio")
|
||||
@@ -11,22 +11,22 @@ const validateProfile = [
|
||||
body("website")
|
||||
.optional()
|
||||
.if(body("website").notEmpty())
|
||||
.matches(URL_REGEX)
|
||||
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||
.withMessage("Invalid website URL."),
|
||||
body("social.twitter")
|
||||
.optional()
|
||||
.if(body("social.twitter").notEmpty())
|
||||
.matches(URL_REGEX)
|
||||
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||
.withMessage("Invalid Twitter URL."),
|
||||
body("social.github")
|
||||
.optional()
|
||||
.if(body("social.github").notEmpty())
|
||||
.matches(URL_REGEX)
|
||||
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||
.withMessage("Invalid Github URL."),
|
||||
body("social.linkedin")
|
||||
.optional()
|
||||
.if(body("social.linkedin").notEmpty())
|
||||
.matches(URL_REGEX)
|
||||
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||
.withMessage("Invalid LinkedIn URL."),
|
||||
body("privacySettings.profileVisibility")
|
||||
.optional()
|
||||
|
||||
@@ -90,7 +90,7 @@ class PointTransaction {
|
||||
const errorContext = createErrorContext('PointTransaction', 'findByUser', { userId, limit, skip });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const result = await couchdbService.find({
|
||||
const docs = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
@@ -99,7 +99,7 @@ class PointTransaction {
|
||||
limit: limit,
|
||||
skip: skip
|
||||
});
|
||||
return result.docs;
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class PointTransaction {
|
||||
const errorContext = createErrorContext('PointTransaction', 'findByType', { transactionType, limit, skip });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const result = await couchdbService.find({
|
||||
const docs = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
transactionType: transactionType
|
||||
@@ -116,7 +116,7 @@ class PointTransaction {
|
||||
limit: limit,
|
||||
skip: skip
|
||||
});
|
||||
return result.docs;
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ class PointTransaction {
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Get the most recent transaction for the user to find current balance
|
||||
const result = await couchdbService.find({
|
||||
const transactions = await couchdbService.find({
|
||||
selector: {
|
||||
type: 'point_transaction',
|
||||
user: userId
|
||||
@@ -153,11 +153,11 @@ class PointTransaction {
|
||||
limit: 1
|
||||
});
|
||||
|
||||
if (result.docs.length === 0) {
|
||||
if (transactions.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result.docs[0].balanceAfter;
|
||||
return transactions[0].balanceAfter;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -180,12 +180,12 @@ class PointTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
const result = await couchdbService.find({
|
||||
const transactions = await couchdbService.find({
|
||||
selector: selector,
|
||||
sort: [{ createdAt: 'desc' }]
|
||||
});
|
||||
|
||||
return result.docs;
|
||||
return transactions;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -222,4 +222,4 @@ class PointTransaction {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PointTransaction;
|
||||
module.exports = PointTransaction;
|
||||
|
||||
@@ -27,7 +27,7 @@ class User {
|
||||
this.avatar = data.avatar || null;
|
||||
this.cloudinaryPublicId = data.cloudinaryPublicId || null;
|
||||
this.bio = data.bio || "";
|
||||
if (this.bio.length > 510) { throw new ValidationError("Bio cannot exceed 500 characters.", "bio", this.bio); }
|
||||
if (this.bio.length > 500) { throw new ValidationError("Bio cannot exceed 500 characters.", "bio", this.bio); }
|
||||
this.location = data.location || "";
|
||||
this.website = data.website || "";
|
||||
if (this.website && !URL_REGEX.test(this.website)) { throw new ValidationError("Invalid website URL.", "website", this.website); }
|
||||
@@ -40,9 +40,9 @@ class User {
|
||||
|
||||
// --- Settings & Preferences ---
|
||||
this.privacySettings = data.privacySettings || { profileVisibility: "public" };
|
||||
if (!["public", "private"].includes(this.privacySettings.profileVisibility)) { throw new ValidationError("Profile visibility must be 'public' or 'private.", "privacySettings.profileVisibility", this.privacySettings.profileVisibility); }
|
||||
if (!["public", "private"].includes(this.privacySettings.profileVisibility)) { throw new ValidationError("Profile visibility must be 'public' or 'private'.", "privacySettings.profileVisibility", this.privacySettings.profileVisibility); }
|
||||
this.preferences = data.preferences || { emailNotifications: true, pushNotifications: true, theme: "light" };
|
||||
if (!["light", "dark"].includes(this.preferences.theme)) { throw new ValidationError("Theme must be light' or 'dark'.", "preferences.theme", this.preferences.theme); }
|
||||
if (!["light", "dark"].includes(this.preferences.theme)) { throw new ValidationError("Theme must be 'light' or 'dark'.", "preferences.theme", this.preferences.theme); }
|
||||
|
||||
|
||||
// --- Gamification & App Data ---
|
||||
@@ -73,9 +73,9 @@ class User {
|
||||
return await withErrorHandling(async () => {
|
||||
let user;
|
||||
if (query.email) { user = await couchdbService.findUserByEmail(query.email); }
|
||||
else if (query._id) { user = await couchdeService.findUserById(query._id); }
|
||||
else if (query._id) { user = await couchdbService.findUserById(query._id); }
|
||||
else { // Generic query fallback
|
||||
const docs = await couchdeService.find({
|
||||
const docs = await couchdbService.find({
|
||||
selector: { type: "user", ...query },
|
||||
limit: 1
|
||||
});
|
||||
@@ -89,7 +89,7 @@ class User {
|
||||
const errorContext = createErrorContext('User', 'findById', { id });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const user = await couchdeService.findUserById(id);
|
||||
const user = await couchdbService.findUserById(id);
|
||||
return user ? new User(user) : null;
|
||||
}, errorContext);
|
||||
}
|
||||
@@ -98,11 +98,11 @@ class User {
|
||||
const errorContext = createErrorContext('User', 'findByIdAndUpdate', { id, update, options });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const user = await couchdeService.findUserById(id);
|
||||
const user = await couchdbService.findUserById(id);
|
||||
if (!user) return null;
|
||||
|
||||
const updatedUser = { ...user, ...update, updatedAt: new Date().toISOString() };
|
||||
const saved = await couchdeService.update(id, updatedUser);
|
||||
const saved = await couchdbService.update(id, updatedUser);
|
||||
|
||||
if (options.new) { return new User(saved); }
|
||||
return new User(user);
|
||||
@@ -113,7 +113,7 @@ class User {
|
||||
const errorContext = createErrorContext('User', 'findByIdAndDelete', { id });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const user = await couchdeService.findUserById(id);
|
||||
const user = await couchdbService.findUserById(id);
|
||||
if (!user) return null;
|
||||
|
||||
await couchdbService.delete(id);
|
||||
@@ -157,7 +157,7 @@ class User {
|
||||
this.updatedAt = new Date().toISOString();
|
||||
if (!this._id) {
|
||||
this._id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
if (this.password && !this.password.startsWith('$2)')) {
|
||||
if (this.password && !this.password.startsWith('$2')) {
|
||||
const salt = await bcrypt.genSalt(10);
|
||||
this.password = await bcrypt.hash(this.password, salt);
|
||||
}
|
||||
@@ -165,7 +165,7 @@ class User {
|
||||
this._rev = created._rev;
|
||||
return this;
|
||||
} else {
|
||||
const updated = await couchdeService.updateDocument(this.toJSON());
|
||||
const updated = await couchdbService.updateDocument(this.toJSON());
|
||||
this._rev = updated._rev;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ const {
|
||||
|
||||
class UserBadge {
|
||||
constructor(userBadgeData) {
|
||||
this.validate(userBadgeData);
|
||||
UserBadge.validate(userBadgeData);
|
||||
Object.assign(this, userBadgeData);
|
||||
}
|
||||
|
||||
validate(userBadgeData, requireEarnedAt = false) {
|
||||
// Validate required fields
|
||||
static validate(userBadgeData, requireEarnedAt = false) {
|
||||
if (!userBadgeData.user || userBadgeData.user.trim() === '') {
|
||||
throw new ValidationError('User field is required', 'user', userBadgeData.user);
|
||||
}
|
||||
@@ -33,7 +32,6 @@ class UserBadge {
|
||||
});
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
// Validate using constructor (earnedAt optional for create)
|
||||
new UserBadge(userBadgeData);
|
||||
|
||||
const doc = {
|
||||
@@ -48,7 +46,8 @@ class UserBadge {
|
||||
};
|
||||
|
||||
const result = await couchdbService.createDocument(doc);
|
||||
return { ...doc, _rev: result.rev };
|
||||
const _rev = result && (result._rev || result.rev);
|
||||
return _rev ? { ...doc, _rev } : doc;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -75,13 +74,10 @@ class UserBadge {
|
||||
const errorContext = createErrorContext('UserBadge', 'find', { filter });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const selector = {
|
||||
type: "user_badge",
|
||||
...filter,
|
||||
};
|
||||
|
||||
const result = await couchdbService.find(selector);
|
||||
return result.docs;
|
||||
const selector = { type: "user_badge", ...filter };
|
||||
const res = await couchdbService.find({ selector });
|
||||
const docs = Array.isArray(res) ? res : (res && Array.isArray(res.docs) ? res.docs : []);
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -89,23 +85,15 @@ class UserBadge {
|
||||
const errorContext = createErrorContext('UserBadge', 'findByUser', { userId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const selector = {
|
||||
type: "user_badge",
|
||||
user: userId,
|
||||
};
|
||||
const selector = { type: "user_badge", user: userId };
|
||||
const res = await couchdbService.find({ selector });
|
||||
const userBadges = Array.isArray(res) ? res : (res && Array.isArray(res.docs) ? res.docs : []);
|
||||
|
||||
const result = await couchdbService.find(selector);
|
||||
const userBadges = result.docs;
|
||||
|
||||
// Populate badge data for each user badge
|
||||
const populatedBadges = await Promise.all(
|
||||
userBadges.map(async (userBadge) => {
|
||||
if (userBadge.badge) {
|
||||
const badge = await couchdbService.getDocument(userBadge.badge);
|
||||
return {
|
||||
...userBadge,
|
||||
badge: badge,
|
||||
};
|
||||
return { ...userBadge, badge };
|
||||
}
|
||||
return userBadge;
|
||||
})
|
||||
@@ -119,21 +107,15 @@ class UserBadge {
|
||||
const errorContext = createErrorContext('UserBadge', 'findByBadge', { badgeId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const selector = {
|
||||
type: "user_badge",
|
||||
badge: badgeId,
|
||||
};
|
||||
|
||||
const result = await couchdbService.find(selector);
|
||||
return result.docs;
|
||||
const selector = { type: "user_badge", badge: badgeId };
|
||||
const res = await couchdbService.find({ selector });
|
||||
const docs = Array.isArray(res) ? res : (res && Array.isArray(res.docs) ? res.docs : []);
|
||||
return docs;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async update(id, updateData) {
|
||||
const errorContext = createErrorContext('UserBadge', 'update', {
|
||||
userBadgeId: id,
|
||||
updateData
|
||||
});
|
||||
const errorContext = createErrorContext('UserBadge', 'update', { userBadgeId: id, updateData });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const doc = await couchdbService.getDocument(id);
|
||||
@@ -141,14 +123,30 @@ class UserBadge {
|
||||
throw new NotFoundError('UserBadge', id);
|
||||
}
|
||||
|
||||
const updatedDoc = {
|
||||
...doc,
|
||||
...updateData,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
const updatedDoc = { ...doc, ...updateData, updatedAt: new Date().toISOString() };
|
||||
|
||||
const result = await couchdbService.createDocument(updatedDoc);
|
||||
return { ...updatedDoc, _rev: result.rev };
|
||||
let result;
|
||||
if (typeof couchdbService.updateDocument === 'function') {
|
||||
result = await couchdbService.updateDocument(updatedDoc);
|
||||
} else if (typeof couchdbService.update === 'function') {
|
||||
result = await couchdbService.update(updatedDoc._id, updatedDoc);
|
||||
} else {
|
||||
throw new DatabaseError('Update method not available on couchdbService');
|
||||
}
|
||||
|
||||
const _rev = result && (result._rev || result.rev);
|
||||
return _rev ? { ...updatedDoc, _rev } : updatedDoc;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findByUserAndBadge(userId, badgeId) {
|
||||
const errorContext = createErrorContext('UserBadge', 'findByUserAndBadge', { userId, badgeId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const selector = { type: "user_badge", user: userId, badge: badgeId };
|
||||
const res = await couchdbService.find({ selector });
|
||||
const docs = Array.isArray(res) ? res : (res && Array.isArray(res.docs) ? res.docs : []);
|
||||
return docs[0] || null;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -161,40 +159,20 @@ class UserBadge {
|
||||
throw new NotFoundError('UserBadge', id);
|
||||
}
|
||||
|
||||
await couchdbService.destroy(id, doc._rev);
|
||||
await couchdbService.deleteDocument(id, doc._rev);
|
||||
return true;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async findByUserAndBadge(userId, badgeId) {
|
||||
const errorContext = createErrorContext('UserBadge', 'findByUserAndBadge', { userId, badgeId });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const selector = {
|
||||
type: "user_badge",
|
||||
user: userId,
|
||||
badge: badgeId,
|
||||
};
|
||||
|
||||
const result = await couchdbService.find(selector);
|
||||
return result.docs[0] || null;
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
static async updateProgress(userId, badgeId, progress) {
|
||||
const errorContext = createErrorContext('UserBadge', 'updateProgress', { userId, badgeId, progress });
|
||||
|
||||
return await withErrorHandling(async () => {
|
||||
const userBadge = await this.findByUserAndBadge(userId, badgeId);
|
||||
|
||||
if (userBadge) {
|
||||
return await this.update(userBadge._id, { progress });
|
||||
} else {
|
||||
return await this.create({
|
||||
user: userId,
|
||||
badge: badgeId,
|
||||
progress,
|
||||
});
|
||||
return await this.create({ user: userId, badge: badgeId, progress });
|
||||
}
|
||||
}, errorContext);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ router.put(
|
||||
if (!user.events.includes(eventId)) {
|
||||
user.events.push(eventId);
|
||||
user.stats.eventsParticipated = user.events.length;
|
||||
await User.update(userId, user);
|
||||
await user.save();
|
||||
}
|
||||
|
||||
// Award points for event participation using couchdbService
|
||||
@@ -218,7 +218,7 @@ router.delete(
|
||||
if (user) {
|
||||
user.events = user.events.filter(id => id !== eventId);
|
||||
user.stats.eventsParticipated = user.events.length;
|
||||
await User.update(userId, user);
|
||||
await user.save();
|
||||
}
|
||||
|
||||
res.json({
|
||||
@@ -313,4 +313,4 @@ router.get(
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user