Merge branch 'develop' of git.digitaltelepresence.com:digital-telepresence/dtp-base into develop

master
rob 11 months ago
commit 3c40f55468

@ -62,6 +62,9 @@ MAILGUN_DOMAIN=
MONGODB_HOST=localhost:27017
MONGODB_DATABASE=dtp-sites
MONGODB_USERNAME=mongo-user
MONGODB_PASSWORD=change-me!
MONGODB_OPTIONS=
#
# Redis configuration

@ -12,6 +12,15 @@ The only qualified operated system for hosting a DTP Sites suite is [Ubuntu 20.0
apt-get -y install build-essential python3-pip
```
## Host Preparation
The following commands must be exeucted on any host expected to run DTP Framework applications.
```sh
apt -y update && apt -y upgrade
apt -y install linux-headers-generic linux-headers-virtual linux-image-virtual linux-virtual
apt -y install build-essential ffmpeg supervisor
```
## Install Data Tier Components
You will need MongoDB and MinIO installed and running before you can start DTP Sites web services.

@ -22,37 +22,36 @@ class UserController extends SiteController {
return next();
});
router.param('userId', this.populateUserId.bind(this));
router.param('localUserId', this.populateLocalUserId.bind(this));
router.post('/local/:localUserId', this.postUpdateLocalUser.bind(this));
router.get('/local/:localUserId', this.getLocalUserView.bind(this));
router.post('/:userId', this.postUpdateUser.bind(this));
router.get('/:userId', this.getUserView.bind(this));
router.get('/', this.getHomeView.bind(this));
return router;
}
async populateUserId (req, res, next, userId) {
async populateLocalUserId (req, res, next, localUserId) {
const { user: userService } = this.dtp.services;
try {
res.locals.userAccount = await userService.getUserAccount(userId);
res.locals.userAccount = await userService.getLocalUserAccount(localUserId);
return next();
} catch (error) {
return next(error);
}
}
async postUpdateUser (req, res, next) {
async postUpdateLocalUser (req, res, next) {
const { user: userService } = this.dtp.services;
try {
await userService.updateForAdmin(res.locals.userAccount, req.body);
await userService.updateLocalForAdmin(res.locals.userAccount, req.body);
res.redirect('/admin/user');
} catch (error) {
return next(error);
}
}
async getUserView (req, res, next) {
async getLocalUserView (req, res, next) {
const { comment: commentService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
@ -68,7 +67,7 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.userAccounts = await userService.getUserAccounts(res.locals.pagination, req.query.u);
res.locals.userAccounts = await userService.searchLocalUserAccounts(res.locals.pagination, req.query.u);
res.locals.totalUserCount = await userService.getTotalCount();
res.render('admin/user/index');
} catch (error) {

@ -78,7 +78,7 @@ class HiveUserController extends SiteController {
throw new SiteError(406, 'Must include search term');
}
res.locals.q = await userService.filterUsername(req.query.q);
res.locals.q = userService.filterUsername(req.query.q);
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.userProfiles = await userService.getUserAccounts(res.locals.pagination, res.locals.q);
res.locals.userProfiles = res.locals.userProfiles.map((user) => {

@ -23,7 +23,11 @@ class UserController extends SiteController {
session: sessionService,
} = dtp.services;
const upload = this.createMulter();
const upload = this.createMulter('user', {
limits: {
fileSize: 1024 * 1000 * 5, // 5MB
},
});
const router = express.Router();
dtp.app.use('/user', router);
@ -60,8 +64,10 @@ class UserController extends SiteController {
return next();
}
router.param('username', this.populateUsername.bind(this));
router.param('userId', this.populateUserId.bind(this));
router.param('localUsername', this.populateLocalUsername.bind(this));
router.param('coreUsername', this.populateCoreUsername.bind(this));
router.param('localUserId', this.populateLocalUserId.bind(this));
router.param('coreUserId', this.populateCoreUserId.bind(this));
router.post(
@ -73,7 +79,7 @@ class UserController extends SiteController {
);
router.post(
'/:userId/profile-photo',
'/:localUserId/profile-photo',
limiterService.createMiddleware(limiterService.config.user.postProfilePhoto),
checkProfileOwner,
upload.single('imageFile'),
@ -81,7 +87,7 @@ class UserController extends SiteController {
);
router.post(
'/:userId/settings',
'/:localUserId/settings',
limiterService.createMiddleware(limiterService.config.user.postUpdateSettings),
checkProfileOwner,
upload.none(),
@ -124,7 +130,7 @@ class UserController extends SiteController {
);
router.get(
'/:userId/settings',
'/:localUsername/settings',
limiterService.createMiddleware(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
@ -132,7 +138,7 @@ class UserController extends SiteController {
this.getUserSettingsView.bind(this),
);
router.get(
'/:username',
'/:localUsername',
limiterService.createMiddleware(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
@ -148,48 +154,84 @@ class UserController extends SiteController {
);
}
async populateUsername (req, res, next, username) {
async populateCoreUsername (req, res, next, coreUsername) {
const { user: userService } = this.dtp.services;
try {
res.locals.userProfile = await userService.getPublicProfile('User', username);
if (!res.locals.userProfile) {
throw new SiteError(404, 'Member not found');
res.locals.username = userService.filterUsername(coreUsername);
res.locals.userProfileId = await userService.getCoreUserId(res.locals.username);
if (!res.locals.userProfileId) {
throw new SiteError(404, 'Core member not found');
}
return next();
// manually chain over to the ID parameter resolver
return this.populateCoreUserId(req, res, next, res.locals.userProfileId);
} catch (error) {
this.log.error('failed to populate username with public profile', { username, error });
this.log.error('failed to populate core username', { coreUsername, error });
return next(error);
}
}
async populateUserId (req, res, next, userId) {
async populateCoreUserId (req, res, next, coreUserId) {
const { user: userService } = this.dtp.services;
try {
userId = mongoose.Types.ObjectId(userId);
} catch (error) {
return next(new SiteError(406, 'Invalid User'));
}
try {
res.locals.userProfile = await userService.getUserAccount(userId);
res.locals.userProfileId = mongoose.Types.ObjectId(coreUserId);
if (req.user && (req.user.type === 'CoreUser') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getCoreUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getCoreUserProfile(res.locals.userProfileId);
}
if (!res.locals.userProfile) {
throw new SiteError(404, 'Core member not found');
}
return next();
} catch (error) {
this.log.error('failed to populate userId', { userId, error });
this.log.error('failed to populate core user id', { coreUserId, error });
return next(error);
}
}
async populateCoreUserId (req, res, next, coreUserId) {
const { coreNode: coreNodeService } = this.dtp.services;
async populateLocalUsername (req, res, next, username) {
const { user: userService } = this.dtp.services;
try {
coreUserId = mongoose.Types.ObjectId(coreUserId);
res.locals.username = userService.filterUsername(username);
res.locals.userProfileId = await userService.getLocalUserId(res.locals.username);
if (!res.locals.userProfileId) {
throw new SiteError(404, 'Local member not found');
}
if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
}
return next();
} catch (error) {
return next(new SiteError(406, 'Invalid User'));
this.log.error('failed to populate local username', { username, error });
return next(error);
}
}
async populateLocalUserId (req, res, next, userId) {
const { user: userService } = this.dtp.services;
try {
res.locals.userProfile = await coreNodeService.getUserByLocalId(coreUserId);
res.locals.userProfileId = mongoose.Types.ObjectId(userId);
if (req.user && (req.user.type === 'User') && req.user._id.equals(res.locals.userProfileId)) {
res.locals.userProfile = await userService.getLocalUserAccount(res.locals.userProfileId);
} else {
res.locals.userProfile = await userService.getLocalUserProfile(res.locals.userProfileId);
}
if (!res.locals.userProfile) {
throw new SiteError(404, 'Local member not found');
}
return next();
} catch (error) {
this.log.error('failed to populate coreUserId', { coreUserId, error });
this.log.error('failed to populate local user id', { userId, error });
return next(error);
}
}
@ -229,8 +271,9 @@ class UserController extends SiteController {
async postProfilePhoto (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('profile-photo');
await userService.updatePhoto(req.user, req.file);
const displayList = this.createDisplayList('profile-photo');
displayList.showNotification(
'Profile photo updated successfully.',
'success',
@ -250,8 +293,9 @@ class UserController extends SiteController {
async postHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('header-image');
await userService.updateHeaderImage(req.user, req.file);
const displayList = this.createDisplayList('header-image');
displayList.showNotification(
'Header image updated successfully.',
'success',
@ -271,10 +315,9 @@ class UserController extends SiteController {
async postUpdateCoreSettings (req, res) {
const { coreNode: coreNodeService } = this.dtp.services;
try {
const displayList = this.createDisplayList('app-settings');
await coreNodeService.updateUserSettings(req.user, req.body);
const displayList = this.createDisplayList('app-settings');
displayList.reload();
res.status(200).json({ success: true, displayList });
} catch (error) {

@ -29,4 +29,6 @@ const AnnouncementSchema = new Schema({
resourceStats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
});
module.exports = mongoose.model('Announcement', AnnouncementSchema);
module.exports = (conn) => {
return conn.model('Announcement', AnnouncementSchema);
};

@ -61,4 +61,6 @@ AttachmentSchema.index({
name: 'attachment_item_idx',
});
module.exports = mongoose.model('Attachment', AttachmentSchema);
module.exports = (conn) => {
return conn.model('Attachment', AttachmentSchema);
};

@ -28,4 +28,6 @@ const ChatMessageSchema = new Schema({
attachments: { type: [Schema.ObjectId], ref: 'Attachment' },
});
module.exports = mongoose.model('ChatMessage', ChatMessageSchema);
module.exports = (conn) => {
return conn.model('ChatMessage', ChatMessageSchema);
};

@ -27,4 +27,6 @@ ChatRoomInviteSchema.index({
name: 'chatroom_invite_unique_idx',
});
module.exports = mongoose.model('ChatRoomInvite', ChatRoomInviteSchema);
module.exports = (conn) => {
return conn.model('ChatRoomInvite', ChatRoomInviteSchema);
};

@ -41,4 +41,6 @@ ChatRoomSchema.index({
name: 'chatroom_public_open_idx',
});
module.exports = mongoose.model('ChatRoom', ChatRoomSchema);
module.exports = (conn) => {
return conn.model('ChatRoom', ChatRoomSchema);
};

@ -72,4 +72,6 @@ CommentSchema.index({
name: 'comment_replies',
});
module.exports = mongoose.model('Comment', CommentSchema);
module.exports = (conn) => {
return conn.model('Comment', CommentSchema);
};

@ -16,4 +16,6 @@ const ConnectTokenSchema = new Schema({
claimed: { type: Date },
});
module.exports = mongoose.model('ConnectToken', ConnectTokenSchema);
module.exports = (conn) => {
return conn.model('ConnectToken', ConnectTokenSchema);
};

@ -28,4 +28,6 @@ ContentReportSchema.index({
name: 'unique_user_content_report',
});
module.exports = mongoose.model('ContentReport', ContentReportSchema);
module.exports = (conn) => {
return conn.model('ContentReport', ContentReportSchema);
};

@ -23,4 +23,6 @@ ContentVoteSchema.index({
name: 'unique_user_content_vote',
});
module.exports = mongoose.model('ContentVote', ContentVoteSchema);
module.exports = (conn) => {
return conn.model('ContentVote', ContentVoteSchema);
};

@ -30,4 +30,6 @@ const CoreNodeConnectSchema = new Schema({
},
});
module.exports = mongoose.model('CoreNodeConnect', CoreNodeConnectSchema);
module.exports = (conn) => {
return conn.model('CoreNodeConnect', CoreNodeConnectSchema);
};

@ -35,4 +35,6 @@ const CoreNodeRequestSchema = new Schema({
},
});
module.exports = mongoose.model('CoreNodeRequest', CoreNodeRequestSchema);
module.exports = (conn) => {
return conn.model('CoreNodeRequest', CoreNodeRequestSchema);
};

@ -45,4 +45,6 @@ CoreNodeSchema.index({
name: 'core_address_idx',
});
module.exports = mongoose.model('CoreNode', CoreNodeSchema);
module.exports = (conn) => {
return conn.model('CoreNode', CoreNodeSchema);
};

@ -52,4 +52,6 @@ CoreUserSchema.index({
name: 'core_username_lc_unique',
});
module.exports = mongoose.model('CoreUser', CoreUserSchema);
module.exports = (conn) => {
return conn.model('CoreUser', CoreUserSchema);
};

@ -17,4 +17,6 @@ const CsrfTokenSchema = new Schema({
ip: { type: String, required: true },
});
module.exports = mongoose.model('CsrfToken', CsrfTokenSchema);
module.exports = (conn) => {
return conn.model('CsrfToken', CsrfTokenSchema);
};

@ -31,4 +31,6 @@ EmailBlacklistSchema.index({
},
});
module.exports = mongoose.model('EmailBlacklist', EmailBlacklistSchema);
module.exports = (conn) => {
return conn.model('EmailBlacklist', EmailBlacklistSchema);
};

@ -12,4 +12,6 @@ const EmailBodySchema = new Schema({
body: { type: String, required: true },
});
module.exports = mongoose.model('EmailBody', EmailBodySchema);
module.exports = (conn) => {
return conn.model('EmailBody', EmailBodySchema);
};

@ -16,4 +16,6 @@ const EmailLogSchema = new Schema({
messageId: { type: String },
});
module.exports = mongoose.model('EmailLog', EmailLogSchema);
module.exports = (conn) => {
return conn.model('EmailLog', EmailLogSchema);
};

@ -15,4 +15,6 @@ const EmailVerifySchema = new Schema({
token: { type: String, required: true },
});
module.exports = mongoose.model('EmailVerify', EmailVerifySchema);
module.exports = (conn) => {
return conn.model('EmailVerify', EmailVerifySchema);
};

@ -17,4 +17,6 @@ const EmailSchema = new Schema({
content: { type: Schema.ObjectId, required: true, index: true, refPath: 'contentType' },
});
module.exports = mongoose.model('Email', EmailSchema);
module.exports = (conn) => {
return conn.model('Email', EmailSchema);
};

@ -36,4 +36,6 @@ const EmojiReactionSchema = new Schema({
timestamp: { type: Number },
});
module.exports = mongoose.model('EmojiReaction', EmojiReactionSchema);
module.exports = (conn) => {
return conn.model('EmojiReaction', EmojiReactionSchema);
};

@ -23,4 +23,6 @@ FeedEntrySchema.index({
name: 'feed_entry_by_feed_idx',
});
module.exports = mongoose.model('FeedEntry', FeedEntrySchema);
module.exports = (conn) => {
return conn.model('FeedEntry', FeedEntrySchema);
};

@ -19,4 +19,6 @@ const FeedSchema = new Schema({
published: { type: Date },
});
module.exports = mongoose.model('Feed', FeedSchema);
module.exports = (conn) => {
return conn.model('Feed', FeedSchema);
};

@ -32,4 +32,6 @@ const ImageSchema = new Schema({
},
});
module.exports = mongoose.model('Image', ImageSchema);
module.exports = (conn) => {
return conn.model('Image', ImageSchema);
};

@ -58,4 +58,6 @@ KaleidoscopeEventSchema.index({
name: 'evtsrc_site_author_index',
});
module.exports = mongoose.model('KaleidoscopeEvent', KaleidoscopeEventSchema);
module.exports = (conn) => {
return conn.model('KaleidoscopeEvent', KaleidoscopeEventSchema);
};

@ -29,4 +29,6 @@ const LogSchema = new Schema({
metadata: { type: Schema.Types.Mixed },
});
module.exports = mongoose.model('Log', LogSchema);
module.exports = (conn) => {
return conn.model('Log', LogSchema);
};

@ -50,4 +50,6 @@ const MediaRouterSchema = new Schema({
}
});
module.exports = mongoose.model('MediaRouter', MediaRouterSchema);
module.exports = (conn) => {
return conn.model('MediaRouter', MediaRouterSchema);
};

@ -40,4 +40,6 @@ const MediaWorkerSchema = new Schema({
}
});
module.exports = mongoose.model('MediaWorker', MediaWorkerSchema);
module.exports = (conn) => {
return conn.model('MediaWorker', MediaWorkerSchema);
};

@ -72,4 +72,6 @@ const NetHostStatsSchema = new Schema({
network: { type: [NetworkInterfaceStatsSchema], required: true },
});
module.exports = mongoose.model('NetHostStats', NetHostStatsSchema);
module.exports = (conn) => {
return conn.model('NetHostStats', NetHostStatsSchema);
};

@ -42,4 +42,6 @@ const NetHostSchema = new Schema({
network: { type: [NetworkInterfaceSchema] },
});
module.exports = mongoose.model('NetHost', NetHostSchema);
module.exports = (conn) => {
return conn.model('NetHost', NetHostSchema);
};

@ -18,4 +18,6 @@ const NewsletterRecipientSchema = new Schema({
},
});
module.exports = mongoose.model('NewsletterRecipient', NewsletterRecipientSchema);
module.exports = (conn) => {
return conn.model('NewsletterRecipient', NewsletterRecipientSchema);
};

@ -28,4 +28,6 @@ const NewsletterSchema = new Schema({
},
});
module.exports = mongoose.model('Newsletter', NewsletterSchema);
module.exports = (conn) => {
return conn.model('Newsletter', NewsletterSchema);
};

@ -16,4 +16,6 @@ const OAuth2AuthorizationCodeSchema = new Schema({
scopes: { type: [String], required: true },
});
module.exports = mongoose.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
module.exports = (conn) => {
return conn.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema);
};

@ -40,4 +40,6 @@ OAuth2ClientSchema.index({
unique: true,
});
module.exports = mongoose.model('OAuth2Client', OAuth2ClientSchema);
module.exports = (conn) => {
return conn.model('OAuth2Client', OAuth2ClientSchema);
};

@ -24,4 +24,6 @@ OAuth2TokenSchema.index({
name: 'oauth2_token_unique',
});
module.exports = mongoose.model('OAuth2Token', OAuth2TokenSchema);
module.exports = (conn) => {
return conn.model('OAuth2Token', OAuth2TokenSchema);
};

@ -33,4 +33,6 @@ OtpAccountSchema.index({
name: 'otp_user_svc_uniq_idx',
});
module.exports = mongoose.model('OtpAccount', OtpAccountSchema);
module.exports = (conn) => {
return conn.model('OtpAccount', OtpAccountSchema);
};

@ -29,4 +29,6 @@ ResourceViewSchema.index({
name: 'res_view_daily_unique',
});
module.exports = mongoose.model('ResourceView', ResourceViewSchema);
module.exports = (conn) => {
return conn.model('ResourceView', ResourceViewSchema);
};

@ -26,4 +26,6 @@ ResourceVisitSchema.index({
name: 'resource_visits_for_user',
});
module.exports = mongoose.model('ResourceVisit', ResourceVisitSchema);
module.exports = (conn) => {
return conn.model('ResourceVisit', ResourceVisitSchema);
};

@ -43,4 +43,6 @@ const StickerSchema = new Schema({
encoded: { type: StickerMediaSchema },
});
module.exports = mongoose.model('Sticker', StickerSchema);
module.exports = (conn) => {
return conn.model('Sticker', StickerSchema);
};

@ -14,4 +14,6 @@ const UserBlockSchema = new Schema({
blockedMembers: { type: [DtpUserSchema] },
});
module.exports = mongoose.model('UserBlock', UserBlockSchema);
module.exports = (conn) => {
return conn.model('UserBlock', UserBlockSchema);
};

@ -24,4 +24,6 @@ const UserNotificationSchema = new Schema({
event: { type: Schema.ObjectId, required: true, ref: 'KaleidoscopeEvent' },
});
module.exports = mongoose.model('UserNotification', UserNotificationSchema);
module.exports = (conn) => {
return conn.model('UserNotification', UserNotificationSchema);
};

@ -24,4 +24,6 @@ const UserSubscriptionSchema = new Schema({
subscriptions: { type: [SubscriptionSchema] },
});
module.exports = mongoose.model('UserSubscription', UserSubscriptionSchema);
module.exports = (conn) => {
return conn.model('UserSubscription', UserSubscriptionSchema);
};

@ -22,17 +22,18 @@ const {
const UserSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1 },
email: { type: String, required: true, lowercase: true, unique: true },
email: { type: String, required: true, lowercase: true, unique: true, select: false },
username: { type: String, required: true },
username_lc: { type: String, required: true, lowercase: true, unique: true, index: 1 },
passwordSalt: { type: String, required: true },
password: { type: String, required: true },
passwordSalt: { type: String, required: true, select: false },
password: { type: String, required: true, select: false },
displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: {
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
header: { type: Schema.ObjectId, ref: 'Image' },
badges: { type: [String] },
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },
@ -62,4 +63,6 @@ UserSchema.virtual('hasAuthorDashboard').get( function ( ) {
this.permissions.canPublishPosts;
});
module.exports = mongoose.model('User', UserSchema);
module.exports = (conn) => {
return conn.model('User', UserSchema);
};

@ -473,7 +473,12 @@ class ChatService extends SiteService {
async createMessage (author, messageDefinition) {
const { sticker: stickerService, user: userService } = this.dtp.services;
author = await userService.getUserAccount(author._id);
this.log.alert('user record', { author });
if (author.type === 'User') {
author = await userService.getLocalUserAccount(author._id);
} else {
author = await userService.getCoreUserAccount(author._id);
}
if (!author || !author.permissions || !author.permissions.canChat) {
throw new SiteError(403, `You are not permitted to chat at all on ${this.dtp.config.site.name}`);
}
@ -744,7 +749,13 @@ class ChatService extends SiteService {
const { user: userService } = this.dtp.services;
const NOW = new Date();
const userCheck = await userService.getUserAccount(user._id);
let userCheck;
if (user.type === '') {
userCheck = await userService.getLocalUserAccount(user._id);
} else {
userCheck = await userService.getCoreUserAccount(user._id);
}
if (!userCheck || !userCheck.permissions || !userCheck.permissions.canChat) {
throw new SiteError(403, 'You are not permitted to chat');
}

@ -92,8 +92,9 @@ class SessionService extends SiteService {
delete user.stats._id;
delete user.optIn._id;
break;
case 'local':
user = await userService.getUserAccount(userId);
user = await userService.getLocalUserAccount(userId);
user.type = 'User';
break;
}

@ -20,6 +20,12 @@ const uuidv4 = require('uuid').v4;
const { SiteError, SiteService } = require('../../lib/site-lib');
/*
* The entire concept of "get a user" is in flux right now. It's best to just
* ignore what's happening in this service right now, and focus on other
* features in the sytem.
*/
class UserService extends SiteService {
constructor (dtp) {
@ -102,25 +108,25 @@ class UserService extends SiteService {
user.password = maskedPassword;
user.flags = {
isAdmin: userDefinition.isAdmin || false,
isModerator: userDefinition.isModerator || false,
isEmailVerified: userDefinition.isEmailVerified || false,
isAdmin: false,
isModerator: false,
isEmailVerified: false,
};
user.permissions = {
canLogin: userDefinition.canLogin || true,
canChat: userDefinition.canChat || true,
canComment: userDefinition.canComment || true,
canReport: userDefinition.canReport || true,
canAuthorPosts: userDefinition.canAuthorPosts || false,
canAuthorPages: userDefinition.canAuthorPages || false,
canPublishPosts: userDefinition.canPublishPosts || false,
canPublishPages: userDefinition.canPublishPages || false,
canLogin: true,
canChat: true,
canComment: true,
canReport: true,
canAuthorPosts: false,
canAuthorPages: false,
canPublishPosts: false,
canPublishPages: false,
};
user.optIn = {
system: userDefinition.optInSystem || true,
marketing: userDefinition.optInMarketing || false,
system: true,
marketing: false,
};
this.log.info('creating new user account', { email: userDefinition.email });
@ -184,7 +190,7 @@ class UserService extends SiteService {
async emailOptOut (userId, category) {
userId = mongoose.Types.ObjectId(userId);
const user = await this.getUserAccount(userId);
const user = await this.getLocalUserAccount(userId);
if (!user) {
throw new SiteError(406, 'Invalid opt-out token');
}
@ -209,7 +215,6 @@ class UserService extends SiteService {
throw SiteError(403, 'Invalid user account operation');
}
// strip characters we don't want to allow in username
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -231,8 +236,7 @@ class UserService extends SiteService {
);
}
async updateForAdmin (user, userDefinition) {
// strip characters we don't want to allow in username
async updateLocalForAdmin (user, userDefinition) {
userDefinition.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
const username_lc = userDefinition.username.toLowerCase();
@ -283,7 +287,6 @@ class UserService extends SiteService {
const updateOp = { $set: { }, $unset: { } };
// strip characters we don't want to allow in username
updateOp.$set.username = striptags(userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''));
if (!updateOp.$set.username || (updateOp.$set.username.length === 0)) {
throw new SiteError(400, 'Must include a username');
@ -321,7 +324,7 @@ class UserService extends SiteService {
}, options);
const accountEmail = account.username.trim().toLowerCase();
const accountUsername = await this.filterUsername(accountEmail);
const accountUsername = this.filterUsername(accountEmail);
this.log.debug('locating user record', { accountEmail, accountUsername });
let user = await User
@ -469,28 +472,23 @@ class UserService extends SiteService {
);
}
filterUserObject (user) {
const filteredUser = {
_id: user._id,
created: user.created,
displayName: user.displayName,
username: user.username,
username_lc: user.username_lc,
bio: user.bio,
flags: user.flags,
permissions: user.permissions,
picture: user.picture,
};
if (filteredUser.flags && filteredUser.flags._id) {
delete filteredUser.flags._id;
async getLocalUserId (username) {
const user = await User.findOne({ username_lc: username }).select('_id').lean();
if (!user) {
return; // undefined
}
if (filteredUser.permissions && filteredUser.permissions._id) {
delete filteredUser.permissions._id;
return user._id;
}
async getCoreUserId (username) {
const user = await CoreUser.findOne({ username_lc: username }).select('_id').lean();
if (!user) {
return; // undefined
}
return filteredUser;
return user._id;
}
async getUserAccount (userId) {
async getLocalUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +optIn +picture')
@ -512,11 +510,47 @@ class UserService extends SiteService {
user.hasAuthorDashboard = user.hasAuthorPermissions || user.hasPublishPermissions;
}
async getUserAccounts (pagination, username) {
async getCoreUserAccount (userId) {
const user = await User
.findById(userId)
.select('+email +flags +permissions +optIn +picture')
.populate(this.populateUser)
.lean();
if (!user) {
throw new SiteError(404, 'Core member account not found');
}
user.type = 'CoreUser';
return user;
}
async getLocalUserProfile (userId) {
const user = await User
.findById(userId)
.select('+email +flags +settings')
.populate(this.populateUser)
.lean();
user.type = 'User';
return user;
}
async getCoreUserProfile (userId) {
const user = await CoreUser
.findById(userId)
.select('+core +flags +settings')
.populate(this.populateUser)
.lean();
user.type = 'CoreUser';
return user;
}
async searchLocalUserAccounts (pagination, username) {
let search = { };
if (username) {
username = this.filterUsername(username);
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
const users = await User
.find(search)
.sort({ username_lc: 1 })
@ -529,60 +563,24 @@ class UserService extends SiteService {
return users.map((user) => { user.type = 'User'; return user; });
}
async getUserProfile (userId) {
let user;
try {
userId = mongoose.Types.ObjectId(userId); // will throw if invalid format
user = User.findById(userId);
} catch (error) {
user = User.findOne({ username: userId });
}
user = await user
.select('+email +flags +settings')
.populate(this.populateUser)
.lean();
return user;
}
async getPublicProfile (type, username) {
if (!username || (typeof username !== 'string')) {
throw new SiteError(406, 'Invalid username');
}
async searchCoreUserAccounts (pagination, username) {
let search = { };
username = username.trim().toLowerCase();
if (username.length === 0) {
throw new SiteError(406, 'Invalid username');
username = this.filterUsername(username);
if (username) {
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
let user;
switch (type) {
case 'CoreUser':
user = await CoreUser
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture header core')
.populate(this.populateUser)
.lean();
if (user) {
user.type = 'CoreUser';
}
break;
case 'User':
user = await User
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture header')
.populate(this.populateUser)
.lean();
if (user) {
user.type = 'User';
}
break;
default:
throw new SiteError(400, 'Invalid user account type');
}
const users = await CoreUser
.find(search)
.sort({ username_lc: 1 })
.select('+core +coreUserId +flags +permissions +optIn')
.skip(pagination.skip)
.limit(pagination.cpp)
.lean()
;
return user;
return users.map((user) => { user.type = 'CoreUser'; return user; });
}
async getRecent (maxCount = 3) {
@ -664,10 +662,6 @@ class UserService extends SiteService {
return actions;
}
async filterUsername (username) {
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
async checkUsername (username) {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
@ -683,6 +677,34 @@ class UserService extends SiteService {
}
}
filterUsername (username) {
while (username[0] === '@') {
username = username.slice(1);
}
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
filterUserObject (user) {
const filteredUser = {
_id: user._id,
created: user.created,
displayName: user.displayName,
username: user.username,
username_lc: user.username_lc,
bio: user.bio,
flags: user.flags,
permissions: user.permissions,
picture: user.picture,
};
if (filteredUser.flags && filteredUser.flags._id) {
delete filteredUser.flags._id;
}
if (filteredUser.permissions && filteredUser.permissions._id) {
delete filteredUser.permissions._id;
}
return filteredUser;
}
async recordProfileView (user, req) {
const { resource: resourceService } = this.dtp.services;
await resourceService.recordView(req, 'User', user._id);
@ -738,6 +760,41 @@ class UserService extends SiteService {
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
}
async updateHeaderImage (user, file) {
const { image: imageService } = this.dtp.services;
await this.removeHeaderImage(user.header);
const images = [
{
width: 1400,
height: 400,
format: 'jpeg',
formatParameters: {
quality: 80,
},
},
];
await imageService.processImageFile(user, file, images);
await User.updateOne(
{ _id: user._id },
{
$set: {
'header': images[0].image._id,
},
},
);
}
async removeHeaderImage (user) {
const { image: imageService } = this.dtp.services;
user = await this.getUserAccount(user._id);
if (user.header) {
await imageService.deleteImage(user.header);
}
await User.updateOne({ _id: user._id }, { $unset: { 'header': '' } });
}
async blockUser (user, blockedUser) {
if (user._id.equals(blockedUser._id)) {
throw new SiteError(406, "You can't block yourself");
@ -812,4 +869,4 @@ module.exports = {
slug: 'user',
name: 'user',
create: (dtp) => { return new UserService(dtp); },
};
};

@ -13,7 +13,7 @@ block content
if userAccount.displayName
.uk-text-large= userAccount.displayName
div
a(href=`/user/${userAccount._id}`) @#{userAccount.username}
a(href=`/user/${userAccount.username}`) @#{userAccount.username}
.uk-card-body
.uk-margin

@ -5,7 +5,7 @@ block content
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-2-3@l")
form(method="POST", action=`/admin/user/${userAccount._id}`).uk-form
form(method="POST", action=`/admin/user/local/${userAccount._id}`).uk-form
input(type="hidden", name="username", value= userAccount.username)
input(type="hidden", name="displayName", value= userAccount.displayName)
.uk-card.uk-card-default.uk-card-small
@ -20,7 +20,7 @@ block content
.uk-width-auto
a(href=`mailto:${userAccount.email}`)= userAccount.email
.uk-width-auto
a(href=`/user/${userAccount._id}`) @#{userAccount.username}
a(href=`/user/${userAccount.username}`) @#{userAccount.username}
.uk-card-body
.uk-margin

@ -22,10 +22,10 @@ block content
each userAccount in userAccounts
tr
td
a(href=`/admin/user/${userAccount._id}`)= userAccount.username
a(href=`/admin/user/local/${userAccount._id}`)= userAccount.username
td
if userAccount.displayName
a(href=`/admin/user/${userAccount._id}`)= userAccount.displayName
a(href=`/admin/user/local/${userAccount._id}`)= userAccount.displayName
else
.uk-text-muted N/A
td= moment(userAccount.created).format('YYYY-MM-DD hh:mm a')

@ -72,7 +72,7 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
i.fas.fa-user
span Profile
li
a(href= user.core ? `/user/core/${user._id}/settings` : `/user/${user._id}/settings`)
a(href= user.core ? `/user/core/${user.username}/settings` : `/user/${user.username}/settings`)
span.nav-item-icon
i.fas.fa-cog
span Settings

@ -68,7 +68,7 @@ mixin renderMenuItem (iconClass, label)
.uk-width-expand Profile
li(class={ "uk-active": (currentView === 'user-settings') })
a(href=`/user/${user._id}/settings`).uk-display-block
a(href=`/user/${user.username}/settings`).uk-display-block
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon

@ -0,0 +1,9 @@
.markdown-block {
font-size: @global-font-size;
line-height: @global-line-height;
color: @global-color;
p:last-of-type {
margin-bottom: 0;
}
}

@ -10,13 +10,14 @@
@import "site/kaleidoscope-event.less";
@import "site/nav.less";
@import "site/button.less";
@import "site/content.less";
@import "site/core-node.less";
@import "site/dashboard.less";
@import "site/site.less";
@import "site/form.less";
@import "site/button.less";
@import "site/sidebar.less";
@import "site/markdown.less";
@import "site/section.less";
@import "site/sidebar.less";
@import "site/site.less";
@import "site/chat.less";

@ -34,6 +34,10 @@ module.grantPermission = async (target, permission) => {
const User = mongoose.model('User');
try {
const user = await User.findOne({ email: target }).select('+permissions +flags');
if (!user) {
throw new Error(`User not found (email: ${target})`);
}
switch (permission) {
case 'admin':
user.flags.isAdmin = true;

@ -43,7 +43,7 @@ module.config = {
module.log = new SiteLog(module, module.config.component);
module.shutdown = async ( ) => {
await SitePlatform.shutdown();
return await SitePlatform.shutdown();
};
(async ( ) => {
@ -63,8 +63,8 @@ module.shutdown = async ( ) => {
process.once('SIGINT', async ( ) => {
module.log.info('SIGINT received');
module.log.info('requesting shutdown...');
await module.shutdown();
const exitCode = await SitePlatform.shutdown();
const exitCode = await module.shutdown();
process.nextTick(( ) => {
process.exit(exitCode);
});

@ -1,4 +1,4 @@
// dtpweb-log.js
// dtp-log.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

@ -1,4 +1,4 @@
// dtpweb-socket.js
// dtp-socket.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

@ -15,12 +15,12 @@ const ConnectToken = mongoose.model('ConnectToken');
const marked = require('marked');
const { SiteLog } = require(path.join(__dirname, 'site-log'));
const { SiteCommon } = require(path.join(__dirname, 'site-common'));
const Events = require('events');
class SiteIoServer extends Events {
class SiteIoServer extends SiteCommon {
constructor (dtp) {
super();
super(dtp, { name: 'ioServer', slug: 'io-server' });
this.dtp = dtp;
this.log = new SiteLog(dtp, DTP_COMPONENT);
}
@ -74,6 +74,10 @@ class SiteIoServer extends Events {
}
async stop ( ) {
if (this.io) {
this.io.close();
delete this.io;
}
}

@ -34,12 +34,22 @@ module.connectDatabase = async (/*dtp*/) => {
host: process.env.MONGODB_HOST,
database: process.env.MONGODB_DATABASE,
});
const mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
const mongoConnectionInfo = {
host: process.env.MONGODB_HOST,
db: process.env.MONGODB_DATABASE,
username: encodeURIComponent(process.env.MONGODB_USERNAME),
password: encodeURIComponent(process.env.MONGODB_PASSWORD),
options: process.env.MONGODB_OPTIONS || '',
};
let mongoConnectUri = `mongodb://${process.env.MONGODB_HOST}/${process.env.MONGODB_DATABASE}`;
if (process.env.NODE_ENV === 'production'){
mongoConnectUri = `mongodb://${mongoConnectionInfo.username}:${mongoConnectionInfo.password}@${mongoConnectionInfo.host}/${mongoConnectionInfo.options}`;
}
module.db = await mongoose.connect(mongoConnectUri, {
socketTimeoutMS: 0,
keepAlive: true,
keepAliveInitialDelay: 300000,
dbName: process.env.MONGODB_DATABASE,
dbName: mongoConnectionInfo.db,
});
module.log.info('connected to MongoDB');
} catch (error) {
@ -48,12 +58,12 @@ module.connectDatabase = async (/*dtp*/) => {
}
};
module.loadModels = async (dtp) => {
dtp.models = module.models = [ ];
const modelScripts = glob.sync(path.join(dtp.config.root, 'app', 'models', '*.js'));
modelScripts.forEach((modelScript) => {
const model = require(modelScript);
const instance = require(modelScript);
const model = instance(module.db);
if (module.models[model.modelName]) {
module.log.error('model name collision', { name: model.modelName });
process.exit(-1);
@ -146,7 +156,7 @@ module.loadControllers = async (dtp) => {
await SiteAsync.each(scripts, async (script) => {
const controller = await require(script);
controller.instance = await controller.create(dtp);
module.log.info('controller loaded', { name: controller.name, slug: controller.slug });
dtp.controllers[controller.name] = controller;
inits.push(controller);
@ -196,7 +206,7 @@ module.exports.startPlatform = async (dtp) => {
await module.connectRedis(dtp);
await module.loadModels(dtp);
SiteLog.setModel(mongoose.model('Log'));
SiteLog.setModel(module.db.model('Log'));
await module.loadServices(dtp);

Loading…
Cancel
Save