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:
@@ -1,6 +1,6 @@
|
|||||||
const { body, validationResult } = require("express-validator");
|
const { body, validationResult } = require("express-validator");
|
||||||
|
|
||||||
const URL_REGEX = /^(https?|ftp):\\/\\/[^\\s\\/$.?#].[^\\s]*$/i;
|
// URL validation handled via express-validator isURL
|
||||||
|
|
||||||
const validateProfile = [
|
const validateProfile = [
|
||||||
body("bio")
|
body("bio")
|
||||||
@@ -11,22 +11,22 @@ const validateProfile = [
|
|||||||
body("website")
|
body("website")
|
||||||
.optional()
|
.optional()
|
||||||
.if(body("website").notEmpty())
|
.if(body("website").notEmpty())
|
||||||
.matches(URL_REGEX)
|
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||||
.withMessage("Invalid website URL."),
|
.withMessage("Invalid website URL."),
|
||||||
body("social.twitter")
|
body("social.twitter")
|
||||||
.optional()
|
.optional()
|
||||||
.if(body("social.twitter").notEmpty())
|
.if(body("social.twitter").notEmpty())
|
||||||
.matches(URL_REGEX)
|
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||||
.withMessage("Invalid Twitter URL."),
|
.withMessage("Invalid Twitter URL."),
|
||||||
body("social.github")
|
body("social.github")
|
||||||
.optional()
|
.optional()
|
||||||
.if(body("social.github").notEmpty())
|
.if(body("social.github").notEmpty())
|
||||||
.matches(URL_REGEX)
|
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||||
.withMessage("Invalid Github URL."),
|
.withMessage("Invalid Github URL."),
|
||||||
body("social.linkedin")
|
body("social.linkedin")
|
||||||
.optional()
|
.optional()
|
||||||
.if(body("social.linkedin").notEmpty())
|
.if(body("social.linkedin").notEmpty())
|
||||||
.matches(URL_REGEX)
|
.isURL({ protocols: ["http", "https", "ftp"], require_protocol: true })
|
||||||
.withMessage("Invalid LinkedIn URL."),
|
.withMessage("Invalid LinkedIn URL."),
|
||||||
body("privacySettings.profileVisibility")
|
body("privacySettings.profileVisibility")
|
||||||
.optional()
|
.optional()
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class User {
|
|||||||
this.avatar = data.avatar || null;
|
this.avatar = data.avatar || null;
|
||||||
this.cloudinaryPublicId = data.cloudinaryPublicId || null;
|
this.cloudinaryPublicId = data.cloudinaryPublicId || null;
|
||||||
this.bio = data.bio || "";
|
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.location = data.location || "";
|
||||||
this.website = data.website || "";
|
this.website = data.website || "";
|
||||||
if (this.website && !URL_REGEX.test(this.website)) { throw new ValidationError("Invalid website URL.", "website", this.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 ---
|
// --- Settings & Preferences ---
|
||||||
this.privacySettings = data.privacySettings || { profileVisibility: "public" };
|
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" };
|
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 ---
|
// --- Gamification & App Data ---
|
||||||
@@ -73,9 +73,9 @@ class User {
|
|||||||
return await withErrorHandling(async () => {
|
return await withErrorHandling(async () => {
|
||||||
let user;
|
let user;
|
||||||
if (query.email) { user = await couchdbService.findUserByEmail(query.email); }
|
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
|
else { // Generic query fallback
|
||||||
const docs = await couchdeService.find({
|
const docs = await couchdbService.find({
|
||||||
selector: { type: "user", ...query },
|
selector: { type: "user", ...query },
|
||||||
limit: 1
|
limit: 1
|
||||||
});
|
});
|
||||||
@@ -89,7 +89,7 @@ class User {
|
|||||||
const errorContext = createErrorContext('User', 'findById', { id });
|
const errorContext = createErrorContext('User', 'findById', { id });
|
||||||
|
|
||||||
return await withErrorHandling(async () => {
|
return await withErrorHandling(async () => {
|
||||||
const user = await couchdeService.findUserById(id);
|
const user = await couchdbService.findUserById(id);
|
||||||
return user ? new User(user) : null;
|
return user ? new User(user) : null;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
@@ -98,11 +98,11 @@ class User {
|
|||||||
const errorContext = createErrorContext('User', 'findByIdAndUpdate', { id, update, options });
|
const errorContext = createErrorContext('User', 'findByIdAndUpdate', { id, update, options });
|
||||||
|
|
||||||
return await withErrorHandling(async () => {
|
return await withErrorHandling(async () => {
|
||||||
const user = await couchdeService.findUserById(id);
|
const user = await couchdbService.findUserById(id);
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const updatedUser = { ...user, ...update, updatedAt: new Date().toISOString() };
|
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); }
|
if (options.new) { return new User(saved); }
|
||||||
return new User(user);
|
return new User(user);
|
||||||
@@ -113,7 +113,7 @@ class User {
|
|||||||
const errorContext = createErrorContext('User', 'findByIdAndDelete', { id });
|
const errorContext = createErrorContext('User', 'findByIdAndDelete', { id });
|
||||||
|
|
||||||
return await withErrorHandling(async () => {
|
return await withErrorHandling(async () => {
|
||||||
const user = await couchdeService.findUserById(id);
|
const user = await couchdbService.findUserById(id);
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
await couchdbService.delete(id);
|
await couchdbService.delete(id);
|
||||||
@@ -157,7 +157,7 @@ class User {
|
|||||||
this.updatedAt = new Date().toISOString();
|
this.updatedAt = new Date().toISOString();
|
||||||
if (!this._id) {
|
if (!this._id) {
|
||||||
this._id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
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);
|
const salt = await bcrypt.genSalt(10);
|
||||||
this.password = await bcrypt.hash(this.password, salt);
|
this.password = await bcrypt.hash(this.password, salt);
|
||||||
}
|
}
|
||||||
@@ -165,7 +165,7 @@ class User {
|
|||||||
this._rev = created._rev;
|
this._rev = created._rev;
|
||||||
return this;
|
return this;
|
||||||
} else {
|
} else {
|
||||||
const updated = await couchdeService.updateDocument(this.toJSON());
|
const updated = await couchdbService.updateDocument(this.toJSON());
|
||||||
this._rev = updated._rev;
|
this._rev = updated._rev;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class UserBadge {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const result = await couchdbService.createDocument(doc);
|
const result = await couchdbService.createDocument(doc);
|
||||||
return { ...doc, _rev: result.rev };
|
return result;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,8 +80,8 @@ class UserBadge {
|
|||||||
...filter,
|
...filter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await couchdbService.find(selector);
|
const docs = await couchdbService.find({ selector });
|
||||||
return result.docs;
|
return docs;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +94,7 @@ class UserBadge {
|
|||||||
user: userId,
|
user: userId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await couchdbService.find(selector);
|
const userBadges = await couchdbService.find({ selector });
|
||||||
const userBadges = result.docs;
|
|
||||||
|
|
||||||
// Populate badge data for each user badge
|
// Populate badge data for each user badge
|
||||||
const populatedBadges = await Promise.all(
|
const populatedBadges = await Promise.all(
|
||||||
@@ -124,8 +123,8 @@ class UserBadge {
|
|||||||
badge: badgeId,
|
badge: badgeId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await couchdbService.find(selector);
|
const docs = await couchdbService.find({ selector });
|
||||||
return result.docs;
|
return docs;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,8 +146,8 @@ class UserBadge {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await couchdbService.createDocument(updatedDoc);
|
const result = await couchdbService.updateDocument(updatedDoc);
|
||||||
return { ...updatedDoc, _rev: result.rev };
|
return result;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +160,7 @@ class UserBadge {
|
|||||||
throw new NotFoundError('UserBadge', id);
|
throw new NotFoundError('UserBadge', id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await couchdbService.destroy(id, doc._rev);
|
await couchdbService.deleteDocument(id, doc._rev);
|
||||||
return true;
|
return true;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
@@ -176,8 +175,8 @@ class UserBadge {
|
|||||||
badge: badgeId,
|
badge: badgeId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await couchdbService.find(selector);
|
const docs = await couchdbService.find({ selector });
|
||||||
return result.docs[0] || null;
|
return docs[0] || null;
|
||||||
}, errorContext);
|
}, errorContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user