diff --git a/.env.default b/.env.default index 32bea87..bffa96a 100644 --- a/.env.default +++ b/.env.default @@ -41,8 +41,8 @@ MINIO_PORT=9000 MINIO_USE_SSL=disabled MINIO_ACCESS_KEY=dtp-webapp MINIO_SECRET_KEY= -MINIO_IMAGE_BUCKET=site-images -MINIO_VIDEO_BUCKET=site-videos +MINIO_IMAGE_BUCKET=webapp-images +MINIO_VIDEO_BUCKET=webapp-videos # # ExpressJS/HTTP configuration @@ -61,8 +61,8 @@ DTP_LOG_MONGODB=enabled DTP_LOG_FILE=enabled DTP_LOG_FILE_PATH=/tmp/dtp-webapp/logs -DTP_LOG_FILE_NAME_APP=justjoeradio-app.log -DTP_LOG_FILE_NAME_HTTP=justjoeradio-access.log +DTP_LOG_FILE_NAME_APP=webapp-app.log +DTP_LOG_FILE_NAME_HTTP=webapp-access.log DTP_LOG_DEBUG=enabled DTP_LOG_INFO=enabled diff --git a/app/models/user.js b/app/models/user.js index c41e734..7b604ad 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -20,8 +20,11 @@ const UserPermissionsSchema = new Schema({ canChat: { type: Boolean, default: true, required: true }, canComment: { type: Boolean, default: true, required: true }, canReport: { type: Boolean, default: true, required: true }, - canAuthorPages: { type: Boolean, default: false, required: true }, - canAuthorPosts: { type: Boolean, default: false, required: true }, +}); + +const UserOptInSchema = new Schema({ + system: { type: Boolean, default: true, requred: true }, + marketing: { type: Boolean, default: true, requred: true }, }); const UserSchema = new Schema({ @@ -32,14 +35,14 @@ const UserSchema = new Schema({ passwordSalt: { type: String, required: true }, password: { type: String, required: true }, displayName: { type: String }, - bio: { type: String, maxlength: 300 }, picture: { large: { type: Schema.ObjectId, ref: 'Image' }, small: { type: Schema.ObjectId, ref: 'Image' }, }, flags: { type: UserFlagsSchema, select: false }, permissions: { type: UserPermissionsSchema, select: false }, + optIn: { type: UserOptInSchema, required: true, select: false }, stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, }); -module.exports = mongoose.model('User', UserSchema); +module.exports = mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/services/user.js b/app/services/user.js index 8cac3b7..1a48f98 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -4,6 +4,8 @@ 'use strict'; +const path = require('path'); + const mongoose = require('mongoose'); const User = mongoose.model('User'); @@ -23,6 +25,8 @@ class UserService { this.dtp = dtp; this.log = new SiteLog(dtp, `svc:${module.exports.slug}`); + this.reservedNames = require(path.join(this.dtp.config.root, 'config', 'reserved-names')); + this.populateUser = [ { path: 'picture.large', @@ -99,13 +103,18 @@ class UserService { canChat: true, canComment: true, canReport: true, - canAuthorPages: false, - canAuthorPosts: false, + }; + + user.optIn = { + system: true, + newsletter: false, }; this.log.info('creating new user account', { email: userDefinition.email }); await user.save(); + await this.sendWelcomeEmail(user); + return user.toObject(); } catch (error) { this.log.error('failed to create user', { error }); @@ -113,6 +122,71 @@ class UserService { } } + async sendWelcomeEmail (user) { + const { email: emailService } = this.dtp.services; + + /* + * Remove all pending EmailVerify tokens for the User. + */ + await emailService.removeVerificationTokensForUser(user); + + /* + * Create the new/only EmailVerify token for the user. This will be the only + * token accepted. Previous emails sent (if they were received) are invalid + * after this. + */ + const verifyToken = await emailService.createVerificationToken(user); + + /* + * Send the welcome email using the new EmailVerify token so it can + * construct a new, valid link to use for verifying the email address. + */ + const templateModel = { + site: this.dtp.config.site, + recipient: user, + emailVerifyToken: verifyToken.token, + }; + const message = { + from: process.env.DTP_EMAIL_SMTP_FROM, + to: user.email, + subject: `Welcome to ${this.dtp.config.site.name}!`, + html: await emailService.renderTemplate('welcome', 'html', templateModel), + text: await emailService.renderTemplate('welcome', 'text', templateModel), + }; + await emailService.send(message); + } + + async setEmailVerification (user, isVerified) { + await User.updateOne( + { _id: user._id }, + { + $set: { 'flags.isEmailVerified': isVerified }, + }, + ); + } + + async emailOptOut (userId, category) { + userId = mongoose.Types.ObjectId(userId); + const user = await this.getUserAccount(userId); + if (!user) { + throw new SiteError(406, 'Invalid opt-out token'); + } + + const updateOp = { $set: { } }; + switch (category) { + case 'marketing': + updateOp.$set['optIn.marketing'] = false; + break; + case 'system': + updateOp.$set['optIn.system'] = false; + break; + default: + throw new SiteError(406, 'Invalid opt-out category'); + } + + await User.updateOne({ _id: userId }, updateOp); + } + async update (user, userDefinition) { if (!user.flags.canLogin) { throw SiteError(403, 'Invalid user account operation'); @@ -123,9 +197,6 @@ class UserService { const username_lc = userDefinition.username.toLowerCase(); userDefinition.displayName = striptags(userDefinition.displayName.trim()); - if (userDefinition.bio) { - userDefinition.bio = striptags(userDefinition.bio.trim()); - } this.log.info('updating user', { userDefinition }); await User.updateOne( @@ -135,7 +206,8 @@ class UserService { username: userDefinition.username, username_lc, displayName: userDefinition.displayName, - bio: userDefinition.bio, + 'optIn.system': userDefinition['optIn.system'] === 'on', + 'optIn.marketing': userDefinition['optIn.marketing'] === 'on', }, }, ); @@ -147,9 +219,6 @@ class UserService { const username_lc = userDefinition.username.toLowerCase(); userDefinition.displayName = striptags(userDefinition.displayName.trim()); - if (userDefinition.bio) { - userDefinition.bio = striptags(userDefinition.bio.trim()); - } this.log.info('updating user for admin', { userDefinition }); await User.updateOne( @@ -159,15 +228,12 @@ class UserService { username: userDefinition.username, username_lc, displayName: userDefinition.displayName, - bio: userDefinition.bio, 'flags.isAdmin': userDefinition.isAdmin === 'on', 'flags.isModerator': userDefinition.isModerator === 'on', 'permissions.canLogin': userDefinition.canLogin === 'on', 'permissions.canChat': userDefinition.canChat === 'on', 'permissions.canComment': userDefinition.canComment === 'on', 'permissions.canReport': userDefinition.canReport === 'on', - 'permissions.canAuthorPages': userDefinition.canAuthorPages === 'on', - 'permissions.canAuthorPosts': userDefinition.canAuthorPosts === 'on', }, }, ); @@ -179,7 +245,6 @@ class UserService { const username_lc = userDefinition.username.toLowerCase(); userDefinition.displayName = striptags(userDefinition.displayName.trim()); - userDefinition.bio = striptags(userDefinition.bio.trim()); this.log.info('updating user settings', { userDefinition }); await User.updateOne( @@ -189,7 +254,6 @@ class UserService { username: userDefinition.username, username_lc, displayName: userDefinition.displayName, - bio: userDefinition.bio, }, }, ); @@ -354,7 +418,7 @@ class UserService { username = username.trim().toLowerCase(); const user = await User .findOne({ username_lc: username }) - .select('_id created username username_lc displayName bio picture header') + .select('_id created username username_lc displayName picture header') .populate(this.populateUser) .lean(); return user; @@ -363,7 +427,7 @@ class UserService { async getRecent (maxCount = 3) { const users = User .find() - .select('_id created username username_lc displayName bio picture') + .select('_id created username username_lc displayName picture') .sort({ created: -1 }) .limit(maxCount) .lean(); @@ -447,28 +511,7 @@ class UserService { if (!username || (typeof username !== 'string') || (username.length === 0)) { throw new SiteError(406, 'Invalid username'); } - const reservedNames = [ - 'about', - 'admin', - 'auth', - 'digitaltelepresence', - 'dist', - 'dtp', - 'fontawesome', - 'fonts', - 'img', - 'image', - 'less', - 'manifest.json', - 'moment', - 'newsletter', - 'numeral', - 'socket.io', - 'uikit', - 'user', - 'welcome', - ]; - if (reservedNames.includes(username.trim().toLowerCase())) { + if (this.reservedNames.includes(username.trim().toLowerCase())) { throw new SiteError(403, 'That username is reserved for system use'); } diff --git a/app/views/admin/user/form.pug b/app/views/admin/user/form.pug index 626a5a3..f853995 100644 --- a/app/views/admin/user/form.pug +++ b/app/views/admin/user/form.pug @@ -18,10 +18,6 @@ block content a(href=`/user/${userAccount._id}`) @#{userAccount.username} .uk-card-body - .uk-margin - label(for="bio").uk-form-label.sr-only Bio - textarea(id="bio", name="bio", rows="4", placeholder= "Bio is empty", disabled= !userAccount.bio || (userAccount.bio.length === 0)).uk-textarea.uk-resize-vertical= userAccount.bio - .uk-margin div(uk-grid) div(class="uk-width-1-1 uk-width-1-2@m") diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 918d4fb..6351911 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -37,9 +37,6 @@ block content .uk-margin label(for="display-name").uk-form-label Display Name input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= user.displayName).uk-input - .uk-margin - label(for="bio").uk-form-label Profile bio - textarea(id="bio", name="bio", rows="3", placeholder="Tell people about yourself").uk-textarea.uk-resize-vertival= user.bio .uk-margin - button(type="submit").uk-button.dtp-button-primary Update account settings + button(type="submit").uk-button.dtp-button-primary Update account settings \ No newline at end of file diff --git a/config/reserved-names.js b/config/reserved-names.js new file mode 100644 index 0000000..8bb5012 --- /dev/null +++ b/config/reserved-names.js @@ -0,0 +1,30 @@ +// reserved-names.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache 2.0 + +'use strict'; + +module.exports = [ + '.env', + '.env-default', + 'about', + 'admin', + 'auth', + 'digitaltelepresence', + 'dist', + 'dtp', + 'fontawesome', + 'fonts', + 'img', + 'image', + 'less', + 'manifest', + 'manifest.json', + 'moment', + 'newsletter', + 'numeral', + 'socket.io', + 'uikit', + 'user', + 'welcome', +]; \ No newline at end of file diff --git a/start-local b/start-local index 7a3d4dd..83688ab 100755 --- a/start-local +++ b/start-local @@ -1,6 +1,6 @@ #!/bin/bash -MINIO_ROOT_USER="base" +MINIO_ROOT_USER="webapp" MINIO_ROOT_PASSWORD="302888b9-c3d8-40f5-92de-6a3c57186af5" export MINIO_ROOT_USER MINIO_ROOT_PASSWORD @@ -10,4 +10,4 @@ forever start --killSignal=SIGINT app/workers/reeeper.js minio server ./data/minio --console-address ":9001" forever stop app/workers/reeeper.js -forever stop app/workers/host-services.js \ No newline at end of file +forever stop app/workers/host-services.js