diff --git a/app/controllers/admin/user.js b/app/controllers/admin/user.js index 1c07b50..4111f3f 100644 --- a/app/controllers/admin/user.js +++ b/app/controllers/admin/user.js @@ -22,6 +22,11 @@ export default class UserAdminController extends SiteController { router.param('userId', this.populateUserId.bind(this)); + router.post( + '/:userId', + this.postUserUpdate.bind(this), + ); + router.get( '/:userId', this.getUserView.bind(this), @@ -49,6 +54,28 @@ export default class UserAdminController extends SiteController { } } + async postUserUpdate (req, res, next) { + const { user: userService } = this.dtp.services; + try { + switch (req.body.action) { + case 'update': + await userService.updateForAdmin(res.locals.userAccount, req.body); + break; + + case 'ban': + if (res.locals.userAccount._id.equals(req.user._id)) { + throw new SiteError(400, "You can't ban yourself"); + } + await userService.banUser(res.locals.userAccount); + break; + } + res.redirect(`/admin/user`); + } catch (error) { + this.log.error('failed to update user', { error }); + return next(error); + } + } + async getUserView (req, res) { res.render('admin/user/view'); } diff --git a/app/controllers/chat.js b/app/controllers/chat.js index 6d78637..4b83463 100644 --- a/app/controllers/chat.js +++ b/app/controllers/chat.js @@ -90,6 +90,12 @@ export default class ChatController extends SiteController { this.getRoomCreateView.bind(this), ); + router.get( + '/room/:roomId/invite', + // limiterService.create(limiterService.config.chat.getRoomInviteForm), + this.getRoomInviteForm.bind(this), + ); + router.get( '/room/:roomId/join', // limiterService.create(limiterService.config.chat.getRoomJoinView), @@ -159,6 +165,7 @@ export default class ChatController extends SiteController { async postMessageReaction (req, res) { const { chat: chatService } = this.dtp.services; try { + await chatService.checkRoomMember(res.locals.room, req.user); await chatService.toggleMessageReaction(req.user, res.locals.message, req.body); return res.status(200).json({ success: true }); } catch (error) { @@ -173,10 +180,7 @@ export default class ChatController extends SiteController { async postRoomMessage (req, res) { const { chat: chatService } = this.dtp.services; try { - this.log.debug('post attachments', { - imageFiles: req.files.imageFiles, - videoFiles: req.files.videoFiles, - }); + await chatService.checkRoomMember(res.locals.room, req.user); await chatService.sendRoomMessage( res.locals.room, @@ -221,6 +225,10 @@ export default class ChatController extends SiteController { res.render('chat/room/create'); } + async getRoomInviteForm (req, res) { + res.render('chat/room/invite'); + } + async getRoomJoinView (req, res, next) { const { chat: chatService } = this.dtp.services; try { @@ -254,6 +262,8 @@ export default class ChatController extends SiteController { async getRoomView (req, res, next) { const { chat: chatService } = this.dtp.services; try { + await chatService.checkRoomMember(res.locals.room, req.user); + res.locals.currentView = 'chat-room'; res.locals.pageTitle = res.locals.room.name; diff --git a/app/services/chat.js b/app/services/chat.js index 4a434d8..5d04db8 100644 --- a/app/services/chat.js +++ b/app/services/chat.js @@ -561,4 +561,34 @@ export default class ChatService extends SiteService { audio: { playSound: 'message-remove' }, }); } + + async removeAllForUser (user) { + this.log.info('removing all chat rooms for user', { + user: { + _id: user._id, + username: user.username, + }, + }); + await ChatRoom + .find({ owner: user._id }) + .populate(this.populateChatRoom) + .cursor() + .eachAsync(async (room) => { + await this.destroyRoom(room); + }); + + this.log.info('removing all chat messages for user', { + user: { + _id: user._id, + username: user.username, + }, + }); + await ChatMessage + .find({ author: user._id }) + .populate(this.populateChatMessage) + .cursor() + .eachAsync(async (message) => { + await this.removeMessage(message); + }); + } } \ No newline at end of file diff --git a/app/services/image.js b/app/services/image.js index d6fe8fc..b84b04b 100644 --- a/app/services/image.js +++ b/app/services/image.js @@ -114,6 +114,22 @@ export default class ImageService extends SiteService { await ChatImage.deleteOne({ _id: image._id }); } + async removeForUser (user) { + this.log.info('removing all images for user', { + user: { + _id: user._id, + username: user.username, + }, + }); + await ChatImage + .find({ owner: user._id }) + .populate(this.populateImage) + .cursor() + .eachAsync(async (image) => { + await this.deleteImage(image); + }); + } + async processImageFile (owner, file, outputs, options) { this.log.debug('processing image file', { owner, file, outputs }); const sharpImage = sharp(file.path); diff --git a/app/services/link.js b/app/services/link.js index b884c5d..0767f41 100644 --- a/app/services/link.js +++ b/app/services/link.js @@ -288,4 +288,23 @@ export default class LinkService extends SiteService { async renderPreview (viewModel) { return this.renderTemplate(this.templates.linkPreview, viewModel); } + + async removeForUser (user) { + this.log.info('removing all links for user', { + user: { + _id: user._id, + username: user.username, + }, + }); + await Link + .find({ submittedBy: user._id }) + .populate(this.populateLink) + .cursor() + .eachAsync(async (link) => { + if (link.submittedBy.length > 1) { + return Link.updateOne({ _id: link._id }, { $pull: { submittedBy: user._id } }); + } + await Link.deleteOne({ _id: link._id }); + }); + } } \ No newline at end of file diff --git a/app/services/otp-auth.js b/app/services/otp-auth.js index 1198407..2d2f4ba 100644 --- a/app/services/otp-auth.js +++ b/app/services/otp-auth.js @@ -238,6 +238,13 @@ export default class OtpAuthService extends SiteService { if (serviceName) { search.service = serviceName; } + this.log.info('removing OTP account(s) for user', { + user: { + _id: user._id, + username: user.username, + }, + serviceName, + }); await OtpAccount.deleteMany(search); } } \ No newline at end of file diff --git a/app/services/user.js b/app/services/user.js index b521c10..0fda94f 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -15,7 +15,6 @@ import PassportLocal from 'passport-local'; import { v4 as uuidv4 } from 'uuid'; import { SiteService, SiteError } from '../../lib/site-lib.js'; -import { users } from 'systeminformation'; export default class UserService extends SiteService { @@ -664,14 +663,43 @@ export default class UserService extends SiteService { }; } + async updateForAdmin (user, userDefinition) { + await User.updateOne( + { _id: user._id }, + { + $set: { + 'flags.isAdmin': userDefinition['flags.isAdmin'] === 'on', + 'flags.isModerator': userDefinition['flags.isModerator'] === 'on', + 'flags.isEmailVerified': userDefinition['flags.isEmailVerified'] === 'on', + 'permissions.canLogin': userDefinition['permissions.canLogin'] === 'on', + 'permissions.canChat': userDefinition['permissions.canChat'] === 'on', + 'permissions.canReport': userDefinition['permissions.canReport'] === 'on', + 'permissions.canShareLinks': userDefinition['permissions.canShareLinks'] === 'on', + 'optIn.system': userDefinition['optIn.system'] === 'on', + 'optIn.marketing': userDefinition['optIn.marketing'] === 'on', + }, + }, + ); + } + async banUser (user) { - const { chat: chatService } = this.dtp.services; + const { + chat: chatService, + image: imageService, + link: linkService, + otpAuth: otpAuthService, + video: videoService, + } = this.dtp.services; const userTag = { _id: user._id, username: user.username }; this.log.alert('banning user', userTag); - this.log.info('removing user chat messages', userTag); - await chatService.deleteAllForUser(user); + await chatService.removeAllForUser(user); + await otpAuthService.removeForUser(user); + await linkService.removeForUser(user); + + await imageService.removeForUser(user); + await videoService.removeForUser(user); this.log.info('removing all user privileges', userTag); await User.updateOne( diff --git a/app/services/video.js b/app/services/video.js index 494e2c3..dcfd611 100644 --- a/app/services/video.js +++ b/app/services/video.js @@ -216,6 +216,22 @@ export default class VideoService extends SiteService { await minioService.removeObject(video.media.bucket, video.media.key); } + async removeForUser (user) { + this.log.info('removing all videos for user', { + user: { + _id: user._id, + username: user.username, + }, + }); + await Video + .find({ owner: user._id }) + .populate(this.populateVideo) + .cursor() + .eachAsync(async (video) => { + await this.removeVideo(video); + }); + } + async transcodeMov (file) { const { media: mediaService } = this.dtp.services; diff --git a/app/views/admin/user/view.pug b/app/views/admin/user/view.pug index afec61e..ab3fe51 100644 --- a/app/views/admin/user/view.pug +++ b/app/views/admin/user/view.pug @@ -3,101 +3,102 @@ block admin-content include ../../user/components/profile-picture - .uk-card.uk-card-default.uk-card-small.uk-border-rounded - .uk-card-header - div(uk-grid).uk-grid-small - .uk-width-auto - +renderProfilePicture(userAccount) + form(method="POST", action=`/admin/user/${userAccount._id}`).uk-form + .uk-card.uk-card-default.uk-card-small.uk-border-rounded + .uk-card-header + div(uk-grid).uk-grid-small + .uk-width-auto + +renderProfilePicture(userAccount) - .uk-width-expand - .uk-margin - .uk-text-lead= user.displayName - div(uk-grid).uk-grid-small.uk-grid-divider - .uk-width-auto @#{user.username} - .uk-width-auto= user.email - .uk-width-auto Created #{dayjs(userAccount.created).fromNow()} on #{dayjs(userAccount.created).format('MMMM D, YYYY')} + .uk-width-expand + .uk-margin + .uk-text-lead= user.displayName + div(uk-grid).uk-grid-small.uk-grid-divider + .uk-width-auto @#{user.username} + .uk-width-auto= user.email + .uk-width-auto Created #{dayjs(userAccount.created).fromNow()} on #{dayjs(userAccount.created).format('MMMM D, YYYY')} - .uk-card-body - .uk-margin - label(for="profile-bio").uk-form-label bio - #profile-bio.markdown-block!= marked.parse(userAccount.bio || '(no bio provided)', { renderer: fullMarkdownRenderer }) - - .uk-margin - label(for="profile-badges").uk-form-label Profile Badges - input(id="profile-badges", type= "text", placeholder= "Comma-separated list of badge names", value= userAccount.badges.join(',')).uk-input + .uk-card-body + .uk-margin + label(for="profile-bio").uk-form-label bio + #profile-bio.markdown-block!= marked.parse(userAccount.bio || '(no bio provided)', { renderer: fullMarkdownRenderer }) + + .uk-margin + label(for="profile-badges").uk-form-label Profile Badges + input(id="profile-badges", type= "text", placeholder= "Comma-separated list of badge names", value= userAccount.badges.join(',')).uk-input - .uk-margin - form(method="POST", action=`/admin/user/${userAccount._id}`).uk-form - .uk-margin - label.uk-form-label Flags - .uk-margin-small - div(uk-grid).uk-grid-small - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="isAdmin", checked= userAccount.flags.isAdmin) - .state.p-success - label Admin - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="isModerator", checked= userAccount.flags.isModerator) - .state.p-success - label Moderator - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="isEmailVerified", checked= userAccount.flags.isEmailVerified) - .state.p-success - label Email Verified + .uk-margin + form(method="POST", action=`/admin/user/${userAccount._id}`).uk-form + .uk-margin + label.uk-form-label Flags + .uk-margin-small + div(uk-grid).uk-grid-small + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="flags.isAdmin", checked= userAccount.flags.isAdmin) + .state.p-success + label Admin + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="flags.isModerator", checked= userAccount.flags.isModerator) + .state.p-success + label Moderator + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="flags.isEmailVerified", checked= userAccount.flags.isEmailVerified) + .state.p-success + label Email Verified - .uk-margin - label.uk-form-label Permissions - .uk-margin-small - div(uk-grid).uk-grid-small - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="canLogin", checked= userAccount.permissions.canLogin) - .state.p-success - label Can Login - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="canChat", checked= userAccount.permissions.canChat) - .state.p-success - label Can Chat - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="canReport", checked= userAccount.permissions.canReport) - .state.p-success - label Can Report - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="canShareLinks", checked= userAccount.permissions.canShareLinks) - .state.p-success - label Can Share Links + .uk-margin + label.uk-form-label Permissions + .uk-margin-small + div(uk-grid).uk-grid-small + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="permissions.canLogin", checked= userAccount.permissions.canLogin) + .state.p-success + label Can Login + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="permissions.canChat", checked= userAccount.permissions.canChat) + .state.p-success + label Can Chat + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="permissions.canReport", checked= userAccount.permissions.canReport) + .state.p-success + label Can Report + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="permissions.canShareLinks", checked= userAccount.permissions.canShareLinks) + .state.p-success + label Can Share Links - .uk-margin - label.uk-form-label Email Opt-In - .uk-margin-small - div(uk-grid).uk-grid-small - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="optIn.system", checked= userAccount.optIn.system) - .state.p-success - label System Messages - .uk-width-auto - .pretty.p-default - input(type="checkbox", name="optIn.marketing", checked= userAccount.optIn.marketing) - .state.p-success - label Marketing + .uk-margin + label.uk-form-label Email Opt-In + .uk-margin-small + div(uk-grid).uk-grid-small + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="optIn.system", checked= userAccount.optIn.system) + .state.p-success + label System Messages + .uk-width-auto + .pretty.p-default + input(type="checkbox", name="optIn.marketing", checked= userAccount.optIn.marketing) + .state.p-success + label Marketing - .uk-card-footer - div(uk-grid).uk-grid-medium - .uk-width-auto - button(type="submit", name="action", value="update").uk-button.uk-button-primary.uk-border-rounded - span - i.fa-solid.fa-save - span.uk-margin-small-left Update User + .uk-card-footer + div(uk-grid).uk-grid-medium + .uk-width-auto + button(type="submit", name="action", value="update").uk-button.uk-button-primary.uk-border-rounded + span + i.fa-solid.fa-save + span.uk-margin-small-left Update User - .uk-width-auto - button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded - span - i.fa-solid.fa-cancel - span.uk-margin-small-left Ban User + .uk-width-auto + button(type="submit", name="action", value="ban").uk-button.uk-button-danger.uk-border-rounded + span + i.fa-solid.fa-cancel + span.uk-margin-small-left Ban User diff --git a/app/views/chat/room/invite.pug b/app/views/chat/room/invite.pug new file mode 100644 index 0000000..3d35d99 --- /dev/null +++ b/app/views/chat/room/invite.pug @@ -0,0 +1,11 @@ +button(type="button", uk-close).uk-modal-close-default +form(method="POST", action=`/chat/room/${room._id}/invite`, style="background: none;").uk-form + .uk-card.uk-card-secondary.uk-card-small + .uk-card-header + h1.uk-card-title Invite New Member + .uk-card-body + label(for="username").uk-form-label Username + input(id="username", name="username", type="text", placeholder="Enter username").uk-input + .uk-card-footer.uk-flex.uk-flex-right + .uk-width-auto + button(type="submit").uk-button.uk-button-primary.uk-border-rounded Invite Member \ No newline at end of file diff --git a/app/views/chat/room/view.pug b/app/views/chat/room/view.pug index 70b380f..23cab33 100644 --- a/app/views/chat/room/view.pug +++ b/app/views/chat/room/view.pug @@ -15,10 +15,10 @@ block view-content .live-username.uk-width-expand .uk-text-truncate= member.displayName || member.username .uk-width-auto - i.fas.fa-volume-off + i.fa-solid.fa-volume-off .uk-width-auto .uk-margin-small-left - i.fas.fa-cog + i.fa-solid.fa-cog block view-navbar @@ -52,12 +52,19 @@ block view-content .uk-width-expand .uk-text-truncate= room.name if room.owner._id.equals(user._id) + .uk-width-auto + a( + href=`/chat/room/${room._id}/invite`, + uk-tooltip={ title: 'Invite people to the room' }, + onclick=`return dtp.app.showForm(event, '/chat/room/${room._id}/invite', 'chat-room-invite')`, + ).uk-link-reset + i.fa-solid.fa-user-plus .uk-width-auto a(href=`/chat/room/${room._id}/settings`, uk-tooltip={ title: 'Configure room settings' }).uk-link-reset - i.fas.fa-cog + i.fa-solid.fa-cog .uk-width-auto a(href="/", uk-tooltip={ title: 'Leave room' }).uk-link-reset - i.fas.fa-person-through-window + i.fa-solid.fa-person-through-window .chat-media div(uk-grid).uk-flex-center diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 1863c32..75dd6fb 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -56,84 +56,75 @@ block view-content ul#account-settings-tabs.uk-switcher li - fieldset - legend Profile - .uk-margin - label(for="username").uk-form-label Username - input(id="username", name="username", type="text", placeholder="Enter username", value= userProfile.username).uk-input - .uk-margin - label(for="display-name").uk-form-label Display Name - input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= userProfile.displayName).uk-input - .uk-margin - label(for="bio").uk-form-label Bio - textarea(id="bio", name="bio", rows="4", placeholder="Enter profile bio").uk-textarea.uk-resize-vertical= userProfile.bio - .uk-margin - label(for="ui-theme").uk-form-label UI Theme - select(id="ui-theme", name="uiTheme").uk-select - option(value="chat-light", selected= (user.ui.theme === 'chat-light')) Light - option(value="chat-dark", selected= (user.ui.theme === 'chat-dark')) Dark + .uk-margin + label(for="username").uk-form-label Username + input(id="username", name="username", type="text", placeholder="Enter username", value= userProfile.username).uk-input + .uk-margin + label(for="display-name").uk-form-label Display Name + input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= userProfile.displayName).uk-input + .uk-margin + label(for="bio").uk-form-label Bio + textarea(id="bio", name="bio", rows="4", placeholder="Enter profile bio").uk-textarea.uk-resize-vertical= userProfile.bio + .uk-margin + label(for="ui-theme").uk-form-label UI Theme + select(id="ui-theme", name="uiTheme").uk-select + option(value="chat-light", selected= (user.ui.theme === 'chat-light')) Light + option(value="chat-dark", selected= (user.ui.theme === 'chat-dark')) Dark li - fieldset - legend Password - .uk-margin - div(uk-grid).uk-grid-small - .uk-width-1-2 - .uk-margin - label(for="password").uk-form-label New Password - input(id="password", name="password", type="password", placeholder="Enter new password", autocomplete= "new-password").uk-input - .uk-width-1-2 - .uk-margin - label(for="passwordv").uk-form-label Verify New Password - input(id="passwordv", name="passwordv", type="password", placeholder="Enter new password again", autocomplete= "new-password").uk-input + .uk-margin + div(uk-grid).uk-grid-small + .uk-width-1-2 + .uk-margin + label(for="password").uk-form-label New Password + input(id="password", name="password", type="password", placeholder="Enter new password", autocomplete= "new-password").uk-input + .uk-width-1-2 + .uk-margin + label(for="passwordv").uk-form-label Verify New Password + input(id="passwordv", name="passwordv", type="password", placeholder="Enter new password again", autocomplete= "new-password").uk-input li - fieldset - legend Email Preferences - - .uk-margin - label(for="email").uk-form-label - span Email Address + .uk-margin + label(for="email").uk-form-label + span Email Address + if user.flags.isEmailVerified + span (verified) + div(uk-grid).uk-grid-small + div(class="uk-width-1-1 uk-width-expand@s") + .uk-margin-small + input(id="email", name="email", type="email", placeholder="Enter email address", value= userProfile.email).uk-input if user.flags.isEmailVerified - span (verified) - div(uk-grid).uk-grid-small - div(class="uk-width-1-1 uk-width-expand@s") - .uk-margin-small - input(id="email", name="email", type="email", placeholder="Enter email address", value= userProfile.email).uk-input - if user.flags.isEmailVerified - .uk-text-small.uk-text-muted Changing your email address will un-verify you and send a new verification email. Check your spam folder! - else - .uk-text-small.uk-text-muted Changing your email address will send a new verification email. Check your spam folder! - div(class="uk-width-1-1 uk-width-auto@s") - button(type="button", onclick="return dtp.app.resendWelcomeEmail(event);").uk-button.uk-button-secondary.uk-border-rounded Resend Welcome Email + .uk-text-small.uk-text-muted Changing your email address will un-verify you and send a new verification email. Check your spam folder! + else + .uk-text-small.uk-text-muted Changing your email address will send a new verification email. Check your spam folder! + div(class="uk-width-1-1 uk-width-auto@s") + button(type="button", onclick="return dtp.app.resendWelcomeEmail(event);").uk-button.uk-button-secondary.uk-border-rounded Resend Welcome Email - .uk-margin - div(uk-grid).uk-grid-small - .uk-width-auto - .pretty.p-switch.p-slim - input(id="optin-system", name="optIn.system", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.system : false) - .state.p-success - label(for="optin-system") System Messages - .uk-width-auto - .pretty.p-switch.p-slim - input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.marketing : false) - .state.p-success - label(for="optin-marketing") Newsletter - .uk-width-auto - .pretty.p-switch.p-slim - input(id="email-verified", type="checkbox", checked= userProfile.flags ? userProfile.flags.isEmailVerified : false, disabled) - .state.p-success - label(for="email-verified") Email Verified + .uk-margin + div(uk-grid).uk-grid-small + .uk-width-auto + .pretty.p-switch.p-slim + input(id="optin-system", name="optIn.system", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.system : false) + .state.p-success + label(for="optin-system") System Messages + .uk-width-auto + .pretty.p-switch.p-slim + input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.marketing : false) + .state.p-success + label(for="optin-marketing") Newsletter + .uk-width-auto + .pretty.p-switch.p-slim + input(id="email-verified", type="checkbox", checked= userProfile.flags ? userProfile.flags.isEmailVerified : false, disabled) + .state.p-success + label(for="email-verified") Email Verified if user.flags && user.flags.isModerator li - fieldset - legend Moderator Preferences - .uk-margin - .pretty.p-switch.p-slim - input(id="moderator-cloaked", name="flags.isCloaked", type="checkbox", checked= userProfile.flags ? userProfile.flags.isCloaked : false) - .state.p-success - label(for="moderator-cloaked") Enable Ghost Mode + .uk-margin + .pretty.p-switch.p-slim + input(id="moderator-cloaked", name="flags.isCloaked", type="checkbox", checked= userProfile.flags ? userProfile.flags.isCloaked : false) + .state.p-success + label(for="moderator-cloaked") Enable Ghost Mode .uk-margin button(type="submit").uk-button.uk-button-primary.uk-border-rounded Update account settings diff --git a/client/css/dtp-site.less b/client/css/dtp-site.less index 4396695..79e5c68 100644 --- a/client/css/dtp-site.less +++ b/client/css/dtp-site.less @@ -1,4 +1,5 @@ @import "site/uk-lightbox.less"; +@import "site/uk-card.less"; @import "site/main.less"; diff --git a/client/css/site/uk-card.less b/client/css/site/uk-card.less new file mode 100644 index 0000000..be8fb66 --- /dev/null +++ b/client/css/site/uk-card.less @@ -0,0 +1,27 @@ +.uk-card { + + &.uk-card-secondary { + + .uk-card-body { + border-top: solid 1px #4a4a4a; + border-bottom: solid 1px #4a4a4a; + } + + button.uk-button { + + &.uk-button-primary { + background: @button-primary-background; + color: @button-primary-color; + + &:hover { + background: @button-primary-hover-background; + color: @button-primary-hover-color; + } + &:active { + background: @button-primary-active-background; + color: @button-primary-active-color; + } + } + } + } +} \ No newline at end of file diff --git a/dtp-chat-cli.js b/dtp-chat-cli.js index 675d367..573f946 100644 --- a/dtp-chat-cli.js +++ b/dtp-chat-cli.js @@ -14,6 +14,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); // jshint ignore:line import mongoose from 'mongoose'; +import randomstring from 'randomstring'; + import { SiteRuntime } from './lib/site-runtime.js'; class SiteTerminalApp extends SiteRuntime { @@ -34,6 +36,10 @@ class SiteTerminalApp extends SiteRuntime { help: 'help [command name]', }, + 'create-account': { + handler: this.createAccount.bind(this), + help: 'create-account email username [password]', + }, 'grant': { handler: this.grant.bind(this), help: 'grant [admin|moderator] username', @@ -101,6 +107,15 @@ class SiteTerminalApp extends SiteRuntime { } } + async createAccount (args) { + const { user: userService } = this.services; + const email = args.shift(); + const username = args.shift(); + const password = args.shift() || randomstring.generate(8); + this.log.info('creating user account', { email, username }); + await userService.create({ email, username, password }); + } + async grant (args) { const User = mongoose.model('User'); diff --git a/package.json b/package.json index 250b439..7258d87 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "pretty-checkbox": "^3.0.3", "pug": "^3.0.2", "qrcode": "^1.5.3", + "randomstring": "^1.3.0", "rate-limiter-flexible": "^5.0.0", "rotating-file-stream": "^3.2.1", "sharp": "^0.33.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d72657c..d4b19b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ dependencies: qrcode: specifier: ^1.5.3 version: 1.5.3 + randomstring: + specifier: ^1.3.0 + version: 1.3.0 rate-limiter-flexible: specifier: ^5.0.0 version: 5.0.0 @@ -5385,12 +5388,23 @@ packages: engines: {node: '>= 0.8'} dev: false + /randombytes@2.0.3: + resolution: {integrity: sha512-lDVjxQQFoCG1jcrP06LNo2lbWp4QTShEXnhActFBwYuHprllQV6VUpwreApsYqCgD+N1mHoqJ/BI/4eV4R2GYg==} + dev: false + /randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: safe-buffer: 5.2.1 dev: true + /randomstring@1.3.0: + resolution: {integrity: sha512-gY7aQ4i1BgwZ8I1Op4YseITAyiDiajeZOPQUbIq9TPGPhUm5FX59izIaOpmKbME1nmnEiABf28d9K2VSii6BBg==} + hasBin: true + dependencies: + randombytes: 2.0.3 + dev: false + /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'}