refactor(models,validators): tighten validation and standardize couchdbService usage\n\n- User: fix bio length check, error messages, and typos (couchdbService calls and bcrypt prefix).\n- UserBadge: use array from find(), return couchdbService results consistently, and call updateDocument/deleteDocument APIs.\n- profileValidator: switch custom URL regex to express-validator isURL with protocol requirement.\n\n🤖 Generated with [AI Assistant]\n\nCo-Authored-By: AI Assistant <noreply@ai-assistant.com>

This commit is contained in:
William Valentin
2025-11-04 10:56:28 -08:00
parent d2e12ef23d
commit 1591f58eec
3 changed files with 27 additions and 28 deletions

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -48,7 +48,7 @@ class UserBadge {
};
const result = await couchdbService.createDocument(doc);
return { ...doc, _rev: result.rev };
return result;
}, errorContext);
}
@@ -80,8 +80,8 @@ class UserBadge {
...filter,
};
const result = await couchdbService.find(selector);
return result.docs;
const docs = await couchdbService.find({ selector });
return docs;
}, errorContext);
}
@@ -94,8 +94,7 @@ class UserBadge {
user: userId,
};
const result = await couchdbService.find(selector);
const userBadges = result.docs;
const userBadges = await couchdbService.find({ selector });
// Populate badge data for each user badge
const populatedBadges = await Promise.all(
@@ -124,8 +123,8 @@ class UserBadge {
badge: badgeId,
};
const result = await couchdbService.find(selector);
return result.docs;
const docs = await couchdbService.find({ selector });
return docs;
}, errorContext);
}
@@ -147,8 +146,8 @@ class UserBadge {
updatedAt: new Date().toISOString(),
};
const result = await couchdbService.createDocument(updatedDoc);
return { ...updatedDoc, _rev: result.rev };
const result = await couchdbService.updateDocument(updatedDoc);
return result;
}, errorContext);
}
@@ -161,7 +160,7 @@ class UserBadge {
throw new NotFoundError('UserBadge', id);
}
await couchdbService.destroy(id, doc._rev);
await couchdbService.deleteDocument(id, doc._rev);
return true;
}, errorContext);
}
@@ -176,8 +175,8 @@ class UserBadge {
badge: badgeId,
};
const result = await couchdbService.find(selector);
return result.docs[0] || null;
const docs = await couchdbService.find({ selector });
return docs[0] || null;
}, errorContext);
}