mirror of
https://github.com/wassname/talk.git
synced 2026-06-29 05:18:32 +08:00
379 lines
9.8 KiB
JavaScript
379 lines
9.8 KiB
JavaScript
const mongoose = require('../services/mongoose');
|
|
const bcrypt = require('bcryptjs');
|
|
const Schema = mongoose.Schema;
|
|
const uuid = require('uuid');
|
|
const TagLinkSchema = require('./schema/tag_link');
|
|
const TokenSchema = require('./schema/token');
|
|
const can = require('../perms');
|
|
const { get } = require('lodash');
|
|
|
|
// USER_ROLES is the array of roles that is permissible as a user role.
|
|
const USER_ROLES = require('./enum/user_roles');
|
|
|
|
// USER_STATUS_USERNAME is the list of statuses that are supported by storing
|
|
// the username state.
|
|
const USER_STATUS_USERNAME = require('./enum/user_status_username');
|
|
|
|
// ProfileSchema is the mongoose schema defined as the representation of a
|
|
// User's profile stored in MongoDB.
|
|
const ProfileSchema = new Schema(
|
|
{
|
|
// ID provides the identifier for the user profile, in the case of a local
|
|
// provider, the id would be an email, in the case of a social provider,
|
|
// the id would be the foreign providers identifier.
|
|
id: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
// Provider is simply the name attached to the authentication mode. In the
|
|
// case of a locally provided profile, this will simply be `local`, or a
|
|
// social provider which for Facebook would just be `facebook`.
|
|
provider: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
// Metadata provides a place to put provider specific details. An example of
|
|
// something that could be stored here is the `metadata.confirmed_at` could be
|
|
// used by the `local` provider to indicate when the email address was
|
|
// confirmed.
|
|
metadata: {
|
|
type: Schema.Types.Mixed,
|
|
},
|
|
},
|
|
{
|
|
_id: false,
|
|
}
|
|
);
|
|
|
|
// UserSchema is the mongoose schema defined as the representation of a User in
|
|
// MongoDB.
|
|
const UserSchema = new Schema(
|
|
{
|
|
// This ID represents the most unique identifier for a user, it is generated
|
|
// when the user is created as a random uuid.
|
|
id: {
|
|
type: String,
|
|
default: uuid.v4,
|
|
unique: true,
|
|
required: true,
|
|
},
|
|
|
|
// This is sourced from the social provider or set manually during user setup
|
|
// and simply provides a name to display for the given user.
|
|
username: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
|
|
// TODO: find a way that we can instead utilize MongoDB 3.4's collation
|
|
// options to build the index in a case insenstive manner:
|
|
// https://docs.mongodb.com/manual/reference/collation/
|
|
lowercaseUsername: {
|
|
type: String,
|
|
required: true,
|
|
unique: true,
|
|
},
|
|
|
|
// This provides a source of identity proof for users who login using the
|
|
// local provider. A local provider will be assumed for users who do not
|
|
// have any social profiles.
|
|
password: String,
|
|
|
|
// Profiles describes the array of identities for a given user. Any one user
|
|
// can have multiple profiles associated with them, including multiple email
|
|
// addresses.
|
|
profiles: [ProfileSchema],
|
|
|
|
// Tokens are the individual personal access tokens for a given user.
|
|
tokens: [TokenSchema],
|
|
|
|
// Role is the specific user role that the user holds.
|
|
role: {
|
|
type: String,
|
|
enum: USER_ROLES,
|
|
required: true,
|
|
default: 'COMMENTER',
|
|
},
|
|
|
|
// Status stores the user status information regarding permissions,
|
|
// capabilities and moderation state.
|
|
status: {
|
|
// Username stores the current user status for the username as well as the
|
|
// history of changes.
|
|
username: {
|
|
// Status stores the current username status.
|
|
status: {
|
|
type: String,
|
|
enum: USER_STATUS_USERNAME,
|
|
},
|
|
|
|
// History stores the history of username status changes.
|
|
history: [
|
|
{
|
|
// Status stores the historical username status.
|
|
status: {
|
|
type: String,
|
|
enum: USER_STATUS_USERNAME,
|
|
},
|
|
|
|
// assigned_by stores the user id of the user who assigned this status.
|
|
assigned_by: { type: String, default: null },
|
|
|
|
// created_at stores the date when this status was assigned.
|
|
created_at: { type: Date, default: Date.now },
|
|
},
|
|
],
|
|
},
|
|
|
|
// Banned stores the current user banned status as well as the history of
|
|
// changes.
|
|
banned: {
|
|
// Status stores the current user banned status.
|
|
status: {
|
|
type: Boolean,
|
|
required: true,
|
|
default: false,
|
|
},
|
|
history: [
|
|
{
|
|
// Status stores the historical banned status.
|
|
status: Boolean,
|
|
|
|
// assigned_by stores the user id of the user who assigned this status.
|
|
assigned_by: { type: String, default: null },
|
|
|
|
// message stores the email content sent to the user.
|
|
message: { type: String, default: null },
|
|
|
|
// created_at stores the date when this status was assigned.
|
|
created_at: { type: Date, default: Date.now },
|
|
},
|
|
],
|
|
},
|
|
|
|
// Suspension stores the current user suspension status as well as the
|
|
// history of changes.
|
|
suspension: {
|
|
// until is the date that the user is suspended until.
|
|
until: {
|
|
type: Date,
|
|
default: null,
|
|
},
|
|
history: [
|
|
{
|
|
// until is the date that the user is suspended until.
|
|
until: Date,
|
|
|
|
// assigned_by stores the user id of the user who assigned this status.
|
|
assigned_by: { type: String, default: null },
|
|
|
|
// message stores the email content sent to the user.
|
|
message: { type: String, default: null },
|
|
|
|
// created_at stores the date when this status was assigned.
|
|
created_at: { type: Date, default: Date.now },
|
|
},
|
|
],
|
|
},
|
|
},
|
|
|
|
// IgnoresUsers is an array of user id's that the current user is ignoring.
|
|
ignoresUsers: [String],
|
|
|
|
// Counts to store related to actions taken on the given user.
|
|
action_counts: {
|
|
default: {},
|
|
type: Object,
|
|
},
|
|
|
|
// Tags are added by the self or by administrators.
|
|
tags: [TagLinkSchema],
|
|
|
|
// Additional metadata stored on the field.
|
|
metadata: {
|
|
default: {},
|
|
type: Object,
|
|
},
|
|
},
|
|
{
|
|
// This will ensure that we have proper timestamps available on this model.
|
|
timestamps: {
|
|
createdAt: 'created_at',
|
|
updatedAt: 'updated_at',
|
|
},
|
|
|
|
toJSON: {
|
|
transform: function(doc, ret) {
|
|
delete ret.__v;
|
|
delete ret._id;
|
|
delete ret.password;
|
|
},
|
|
},
|
|
}
|
|
);
|
|
|
|
// Add the index on the user profile data.
|
|
UserSchema.index(
|
|
{
|
|
'profiles.id': 1,
|
|
'profiles.provider': 1,
|
|
},
|
|
{
|
|
unique: true,
|
|
background: false,
|
|
}
|
|
);
|
|
|
|
UserSchema.index(
|
|
{
|
|
lowercaseUsername: 1,
|
|
'profiles.id': 1,
|
|
created_at: -1,
|
|
},
|
|
{
|
|
background: true,
|
|
}
|
|
);
|
|
|
|
// This query is executed often, to count the number of flagged accounts with
|
|
// usernames.
|
|
UserSchema.index(
|
|
{
|
|
'action_counts.flag': 1,
|
|
'status.username.status': 1,
|
|
},
|
|
{
|
|
background: true,
|
|
}
|
|
);
|
|
|
|
// Sorting users by created at is the default people search.
|
|
UserSchema.index(
|
|
{
|
|
created_at: -1,
|
|
},
|
|
{
|
|
background: true,
|
|
}
|
|
);
|
|
|
|
/**
|
|
* returns true if a commenter is staff
|
|
*/
|
|
UserSchema.method('isStaff', function() {
|
|
return this.role !== 'COMMENTER';
|
|
});
|
|
|
|
/**
|
|
* This verifies that a password is valid.
|
|
*/
|
|
UserSchema.method('verifyPassword', function(password) {
|
|
return new Promise((resolve, reject) => {
|
|
bcrypt.compare(password, this.password, (err, res) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
|
|
if (!res) {
|
|
return resolve(false);
|
|
}
|
|
|
|
return resolve(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Can returns true if the user is allowed to perform a specific graph
|
|
* operation.
|
|
*/
|
|
UserSchema.method('can', function(...actions) {
|
|
return can(this, ...actions);
|
|
});
|
|
|
|
/**
|
|
* firstEmail will return the first email on the user.
|
|
*/
|
|
UserSchema.virtual('firstEmail').get(function() {
|
|
const emails = this.emails;
|
|
if (emails.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return emails[0];
|
|
});
|
|
|
|
/**
|
|
* emails will return all the emails on a user.
|
|
*/
|
|
UserSchema.virtual('emails').get(function() {
|
|
return (this.profiles || [])
|
|
.filter(({ provider }) => provider === 'local')
|
|
.map(({ id }) => id);
|
|
});
|
|
|
|
/**
|
|
* hasVerifiedEmail will return true if at least one of the local email accounts
|
|
* have their email verified.
|
|
*/
|
|
UserSchema.virtual('hasVerifiedEmail').get(function() {
|
|
return this.profiles
|
|
.filter(({ provider }) => provider === 'local')
|
|
.some(profile => {
|
|
const confirmedAt = get(profile, 'metadata.confirmed_at') || null;
|
|
|
|
// If the profile doesn't have a metadata field, or it does not have a
|
|
// confirmed_at field, or that field is null, then send them back.
|
|
return confirmedAt !== null;
|
|
});
|
|
});
|
|
|
|
UserSchema.virtual('system')
|
|
.get(function() {
|
|
return this._system;
|
|
})
|
|
.set(function(system) {
|
|
this._system = system;
|
|
});
|
|
|
|
/**
|
|
* banned returns true when the user is currently banned, and sets the banned
|
|
* status locally.
|
|
*/
|
|
UserSchema.virtual('banned')
|
|
.get(function() {
|
|
return this.status.banned.status;
|
|
})
|
|
.set(function(status) {
|
|
this.status.banned.status = status;
|
|
this.status.banned.history.push({
|
|
status,
|
|
created_at: new Date(),
|
|
});
|
|
});
|
|
|
|
/**
|
|
* suspended returns true when the user is currently suspended, and sets the
|
|
* suspension status locally.
|
|
*/
|
|
UserSchema.virtual('suspended')
|
|
.get(function() {
|
|
return Boolean(
|
|
this.status.suspension.until && this.status.suspension.until > new Date()
|
|
);
|
|
})
|
|
.set(function(until) {
|
|
this.status.suspension.until = until;
|
|
this.status.suspension.history.push({
|
|
until,
|
|
created_at: new Date(),
|
|
});
|
|
});
|
|
|
|
// Create the User model.
|
|
const UserModel = mongoose.model('User', UserSchema);
|
|
|
|
module.exports = UserModel;
|