Browse Source

I wrote code.

develop
Rob Colbert 1 year ago
parent
commit
a47f25bfec
  1. 18
      app/controllers/auth.js
  2. 1
      app/controllers/home.js
  3. 19
      app/models/action-audit.js
  4. 15
      app/models/chat-filter.js
  5. 30
      app/services/action-audit.js
  6. 183
      app/services/display-engine.js
  7. 13
      app/services/text.js
  8. 6
      app/services/user.js
  9. 8
      app/views/components/navbar.pug
  10. 16
      app/views/home.pug
  11. 13
      app/views/layout/main.pug
  12. 8
      client/css/dtp-dark.less
  13. 8
      client/css/dtp-light.less
  14. 3
      client/css/dtp-site.less
  15. 15
      client/css/main.less
  16. 10
      client/css/site/image.less
  17. 8
      client/css/site/main.less
  18. 62
      client/css/site/stage.less
  19. 117
      client/css/site/uikit-theme.dtp-dark.less
  20. 35
      client/css/site/uikit-theme.dtp-light.less
  21. 2
      client/css/site/variables.less
  22. 9
      webpack.config.js

18
app/controllers/auth.js

@ -116,7 +116,10 @@ export default class AuthController extends SiteController {
} }
async postOtpEnable (req, res, next) { async postOtpEnable (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
actionAudit: actionAuditService,
otpAuth: otpAuthService,
} = this.dtp.services;
const service = req.body['otp-service']; const service = req.body['otp-service'];
const secret = req.body['otp-secret']; const secret = req.body['otp-secret'];
@ -127,6 +130,7 @@ export default class AuthController extends SiteController {
this.log.info('enabling OTP protections', { service, secret, token }); this.log.info('enabling OTP protections', { service, secret, token });
res.locals.otpAccount = await otpAuthService.createOtpAccount(req, service, secret, token); res.locals.otpAccount = await otpAuthService.createOtpAccount(req, service, secret, token);
res.locals.otpRedirectURL = otpRedirectURL; res.locals.otpRedirectURL = otpRedirectURL;
await actionAuditService.auditRequest(req, 'Enabled 2FA');
res.render('otp/new-account'); res.render('otp/new-account');
} catch (error) { } catch (error) {
this.log.error('failed to enable OTP protections', { this.log.error('failed to enable OTP protections', {
@ -137,7 +141,10 @@ export default class AuthController extends SiteController {
} }
async postOtpAuthenticate (req, res, next) { async postOtpAuthenticate (req, res, next) {
const { otpAuth: otpAuthService } = this.dtp.services; const {
actionAudit: actionAuditService,
otpAuth: otpAuthService,
} = this.dtp.services;
if (!req.user) { if (!req.user) {
return res.status(403).json({ return res.status(403).json({
@ -161,6 +168,7 @@ export default class AuthController extends SiteController {
} }
try { try {
await otpAuthService.startOtpSession(req, service, passcode); await otpAuthService.startOtpSession(req, service, passcode);
await actionAuditService.auditRequest(req, '2FA credentials provided successfully');
return res.redirect(req.body['otp-redirect']); return res.redirect(req.body['otp-redirect']);
} catch (error) { } catch (error) {
this.log.error('failed to verify one-time password for 2FA', { this.log.error('failed to verify one-time password for 2FA', {
@ -241,6 +249,7 @@ export default class AuthController extends SiteController {
} }
async getSocketToken (req, res, next) { async getSocketToken (req, res, next) {
const { actionAudit: actionAuditService } = this.dtp.services;
try { try {
const token = await ConnectToken.create({ const token = await ConnectToken.create({
created: new Date(), created: new Date(),
@ -250,6 +259,7 @@ export default class AuthController extends SiteController {
consumer: req.user._id, consumer: req.user._id,
token: uuidv4(), token: uuidv4(),
}); });
await actionAuditService.auditRequest(req, 'Received socket connect token');
res.status(200).json({ res.status(200).json({
success: true, success: true,
token: token.token token: token.token
@ -319,9 +329,13 @@ export default class AuthController extends SiteController {
} }
async getLogout (req, res, next) { async getLogout (req, res, next) {
const { actionAudit: actionAuditService } = this.dtp.services;
if (!req.user) { if (!req.user) {
return next(new SiteError(403, 'You are not signed in')); return next(new SiteError(403, 'You are not signed in'));
} }
await actionAuditService.auditRequest(req, 'Logged out');
req.logout((err) => { req.logout((err) => {
if (err) { if (err) {
this.log.error('failed to destroy browser session', { err }); this.log.error('failed to destroy browser session', { err });

1
app/controllers/home.js

@ -36,6 +36,7 @@ export default class HomeController extends SiteController {
if (!req.user) { if (!req.user) {
return res.redirect('/welcome'); return res.redirect('/welcome');
} }
res.locals.currentView = 'home';
res.locals.pageDescription = 'DTP Chat Home'; res.locals.pageDescription = 'DTP Chat Home';
res.render('home'); res.render('home');
} }

19
app/models/action-audit.js

@ -0,0 +1,19 @@
// action-audit.js
// Copyright (C) 2024 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const ActionAuditSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1, expires: '90d' },
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
ip: { type: String },
targetType: { type: String },
target: { type: Schema.ObjectId, index: 1, refPath: 'targetType' },
note: { type: String },
});
export default mongoose.model('ActionAudit', ActionAuditSchema);

15
app/models/chat-filter.js

@ -0,0 +1,15 @@
// chat-filter.js
// Copyright (C) 2022,2023 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import mongoose from 'mongoose';
const Schema = mongoose.Schema;
const ChatFilterSchema = new Schema({
created: { type: Date, required: true, default: Date.now, index: 1, expires: '300d' },
filter: { type: String, required: true, unique: true, lowercase: true, index: 1 },
});
export default mongoose.model('ChatFilter', ChatFilterSchema);

30
app/services/action-audit.js

@ -0,0 +1,30 @@
// action-audit.js
// Copyright (C) 2024 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import mongoose from 'mongoose';
const ActionAudit = mongoose.model('ActionAudit');
import { SiteService } from '../../lib/site-lib.js';
export default class ActionAuditService extends SiteService {
static get slug () { return 'actionAudit'; }
static get name ( ) { return 'ActionAuditService'; }
constructor (dtp) {
super(dtp, ActionAuditService);
}
async auditRequest (req, note) {
const NOW = new Date();
const audit = new ActionAudit();
audit.created = NOW;
audit.user = req.user;
audit.ip = req.ip;
audit.note = note;
await audit.save();
}
}

183
app/services/display-engine.js

@ -0,0 +1,183 @@
// display-engine.js
// Copyright (C) 2022,2023 DTP Technologies, LLC
// All Rights Reserved
'use strict';
import path from 'node:path';
import pug from 'pug';
import { v4 as uuidv4 } from 'uuid';
import { SiteService, SiteError } from '../../lib/site-lib.js';
class DisplayList {
constructor (service, name) {
this.name = name;
this.id = uuidv4();
this.commands = [ ];
}
showNotification (message, status, pos, timeout) {
this.commands.push({
action: 'showNotification',
params: { message, status, pos, timeout },
});
}
showModal (html) {
this.commands.push({
action: 'showModal',
params: { html },
});
}
playNotificationSound (action) {
this.commands.push({
action: 'playNotificationSound',
params: { action },
});
}
playSound (soundId) {
this.commands.push({
action: 'playSound',
params: { soundId },
});
}
addElement (selector, where, html) {
this.commands.push({
selector, action: 'addElement',
params: { where, html },
});
}
removeChildren (selector) {
this.commands.push({
selector, action: 'removeChildren',
params: { },
});
}
setTextContent (selector, text) {
this.commands.push({
selector, action: 'setTextContent',
params: { text },
});
}
replaceElement (selector, html) {
this.commands.push({
selector, action: 'replaceElement',
params: { html },
});
}
removeElement (selector) {
this.commands.push({
selector, action: 'removeElement',
params: { },
});
}
setAttribute (selector, name, value) {
this.commands.push({
selector, action: 'setAttribute',
params: { name, value },
});
}
removeAttribute (selector, name) {
this.commands.push({
selector, action: 'removeAttribute',
params: { name },
});
}
toggleAttribute (selector, name, force) {
this.commands.push({
selector, action: 'toggleAttribute',
params: { name, force },
});
}
addClass (selector, add) {
this.commands.push({
selector, action: 'addClass',
params: { add },
});
}
removeClass (selector, remove) {
this.commands.push({
selector, action: 'removeClass',
params: { remove },
});
}
replaceClass (selector, remove, add) {
this.commands.push({
selector, action: 'replaceClass',
params: { remove, add },
});
}
setValue (selector, value) {
this.commands.push({
selector, action: 'setValue',
params: { value },
});
}
navigateTo (href) {
this.commands.push({
action: 'navigateTo',
params: { href },
});
}
navigateBack ( ) {
this.commands.push({
action: 'navigateBack',
params: { },
});
}
reloadView ( ) {
this.commands.push({
action: 'reloadView',
params: { },
});
}
}
export default class DisplayEngineService extends SiteService {
static get name ( ) { return 'DisplayEngineService'; }
static get slug ( ) { return 'displayEngine'; }
constructor (dtp) {
super(dtp, DisplayEngineService);
this.templates = { };
}
loadTemplate (name, pugScript) {
const scriptFile = path.join(this.dtp.config.root, 'app', 'views', pugScript);
this.templates[name] = pug.compileFile(scriptFile);
}
executeTemplate (name, data) {
if (!this.templates[name]) {
this.log.error('view engine template undefined', { name });
throw new SiteError(500, 'Unknown display engine template');
}
data = Object.assign(this.dtp.app.locals, data);
return this.templates[name](data);
}
createDisplayList (name = 'default') {
return new DisplayList(this, name);
}
}

13
app/services/text.js

@ -4,6 +4,9 @@
'use strict'; 'use strict';
import mongoose from 'mongoose';
const ChatFilter = mongoose.model('ChatFilter');
import striptags from 'striptags'; import striptags from 'striptags';
import unzalgo from 'unzalgo'; import unzalgo from 'unzalgo';
import shoetest from 'shoetest'; import shoetest from 'shoetest';
@ -21,6 +24,10 @@ export default class TextService extends SiteService {
super(dtp, TextService); super(dtp, TextService);
} }
async start ( ) {
await this.loadChatFilters();
}
/** /**
* Basic text cleaning function to remove Zalgo and tags. * Basic text cleaning function to remove Zalgo and tags.
* @param {String} text The text to be cleaned * @param {String} text The text to be cleaned
@ -68,4 +75,10 @@ export default class TextService extends SiteService {
*/ */
return this.clean(text); return this.clean(text);
} }
async loadChatFilters ( ) {
this.chatFilters = await ChatFilter.find().lean();
this.chatFilters = this.chatFilters.map((filter) => filter.filter);
this.log.debug('loading chat filters', { count: this.chatFilters.length });
}
} }

6
app/services/user.js

@ -392,6 +392,8 @@ export default class UserService extends SiteService {
} }
async login (req, res, next, options) { async login (req, res, next, options) {
const { actionAudit: actionAuditService } = this.dtp.services;
options = Object.assign({ options = Object.assign({
loginUrl: '/welcome/login', loginUrl: '/welcome/login',
}, options); }, options);
@ -408,6 +410,7 @@ export default class UserService extends SiteService {
} }
return res.redirect(options.loginUrl); return res.redirect(options.loginUrl);
} }
this.log.alert('user login', { this.log.alert('user login', {
user: { user: {
_id: user._id, _id: user._id,
@ -415,10 +418,11 @@ export default class UserService extends SiteService {
ip: req.ip, ip: req.ip,
} }
}); });
req.login(user, (error) => { req.login(user, async (error) => {
if (error) { if (error) {
return next(error); return next(error);
} }
await actionAuditService.auditRequest(req, 'User login');
if (options.onLoginSuccess) { if (options.onLoginSuccess) {
return options.onLoginSuccess(req, res, next); return options.onLoginSuccess(req, res, next);
} }

8
app/views/components/navbar.pug

@ -51,3 +51,11 @@ nav(style="background: #000000;").uk-navbar-container.uk-light
span.nav-item-icon span.nav-item-icon
i.fas.fa-user-lock i.fas.fa-user-lock
span Admin span Admin
li.uk-nav-divider
li
a(href=`/auth/logout`)
span.nav-item-icon
i.fas.fa-right-from-bracket
span Sign Out

16
app/views/home.pug

@ -1,7 +1,15 @@
extends layout/main extends layout/main
block view-content block view-content
section.uk-section.uk-section-default.uk-section-small .dtp-chat-stage
.uk-container .chat-sidebar
h1= site.name .chat-stage-header Active Members
div= site.description .sidebar-panel This is a sidebar content panel. It should word-wrap correctly and will be used to display usernames in the room with status.
.chat-stage-header Idle Members
.sidebar-panel This is a sidebar content panel. It should word-wrap correctly and will be used to display usernames in the room with status.
.chat-container
.chat-stage-header Chat Room and Host names go here.
.chat-content-panel This is the chat content panel. It should word-wrap and scroll correctly, and will be where individual chat messages will render as they arrive and are sent.
.chat-input-panel This is the chat input panel. It will be where text is entered and sent, and contain some menu items, icons, and buttons.

13
app/views/layout/main.pug

@ -19,7 +19,7 @@ html(lang='en', data-obs-widget= obsWidget)
block vendorcss block vendorcss
link(rel='stylesheet', href=`/dist/chat-client.css?v=${pkg.version}`) link(rel='stylesheet', href=`/dist/chat-light.css?v=${pkg.version}`)
link(rel='stylesheet', href=`/pretty-checkbox/pretty-checkbox.min.css?v=${pkg.version}`) link(rel='stylesheet', href=`/pretty-checkbox/pretty-checkbox.min.css?v=${pkg.version}`)
block viewcss block viewcss
@ -56,7 +56,16 @@ html(lang='en', data-obs-widget= obsWidget)
} }
} }
body.dtp(class= 'dtp-dark', data-dtp-env= process.env.NODE_ENV, data-dtp-domain= site.domainKey, data-current-view= currentView, data-is-popout= isPopOutView, data-obs-widget= obsWidget, data-embed-widget= embedWidget) body.dtp(
class= 'dtp-dark',
data-dtp-env= process.env.NODE_ENV,
data-dtp-domain= site.domainKey,
data-current-view= currentView,
data-is-popout= isPopOutView,
data-obs-widget= obsWidget,
data-embed-widget= embedWidget,
)
block view-navbar block view-navbar
include ../components/navbar include ../components/navbar

8
client/css/dtp-dark.less

@ -0,0 +1,8 @@
@import "site/variables.less";
@import "uikit/src/less/uikit.less";
@import "node_modules/uikit/src/less/uikit.theme.less";
@import "site/uikit-theme.dtp-dark.less";
@import "dtp-site.less";

8
client/css/dtp-light.less

@ -0,0 +1,8 @@
@import "site/variables.less";
@import "uikit/src/less/uikit.less";
@import "node_modules/uikit/src/less/uikit.theme.less";
@import "site/uikit-theme.dtp-light.less";
@import "dtp-site.less";

3
client/css/dtp-site.less

@ -0,0 +1,3 @@
@import "site/main.less";
@import "site/stage.less";
@import "site/image.less";

15
client/css/main.less

@ -1,15 +0,0 @@
@import "uikit/src/less/uikit.less";
html, body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
img.navbar-logo {
width: auto;
height: 48px;
}
img.profile-navbar {
width: auto;
height: 48px;
border-radius: 5px;
}

10
client/css/site/image.less

@ -0,0 +1,10 @@
img.navbar-logo {
width: auto;
height: 48px;
}
img.profile-navbar {
width: auto;
height: 48px;
border-radius: 5px;
}

8
client/css/site/main.less

@ -0,0 +1,8 @@
html, body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
&[data-current-view="home"] {
position: fixed;
top: 0; right: 0; bottom: 0; left: 0;
}
}

62
client/css/site/stage.less

@ -0,0 +1,62 @@
@stage-panel-padding: 4px 10px;
@stage-border-color: #4a4a4a;
.dtp-chat-stage {
position: absolute;
top: @site-navbar-height; right: 0; bottom: 0; left: 0;
width: 100%;
height: 100%;
display: flex;
.chat-stage-header {
flex-grow: 0;
flex-shrink: 0;
padding: @stage-panel-padding;
font-weight: bold;
background-color: #2a2a2a;
color: #e8e8e8;
}
.chat-sidebar {
box-sizing: border-box;
width: 240px;
flex-shrink: 0;
flex-grow: 0;
background-color: #1a1a1a;
color: #e8e8e8;
border-right: solid 1px @stage-border-color;
.sidebar-panel {
padding: @stage-panel-padding;
margin-bottom: 10px;
color: inherit;
}
}
.chat-container {
display: flex;
flex-grow: 1;
flex-direction: column;
background-color: #e8e8e8;
color: #1a1a1a;
.chat-content-panel {
padding: @stage-panel-padding;
flex-grow: 1;
}
.chat-input-panel {
height: 200px;
padding: @stage-panel-padding;
background-color: #1a1a1a;
color: #e8e8e8;
}
}
}

117
client/css/site/uikit-theme.dtp-dark.less

@ -0,0 +1,117 @@
//
// Colors
//
@page-background-color: #000000;
@content-background-color: #2a2a2a;
@content-border-color: #4a4a4a;
@content-container-color: #2a2a2a;
@site-brand-color: #ff0013;
@button-label-color: #e8e8e8;
@button-label-hover: #2a2a2a;
@social-link-color: #e8e8e8;
@checkout-button-text-color: #e8e8e8;
@emoji-react-button-active-color: #adc7a0;
@scrollbar-border-color: @content-border-color;
@scrollbar-thumb-color: #ff001380;
@global-background: #1a1a1a;
@global-muted-background: #3a3a3a;
@global-primary-background: #1e87f0;
@global-secondary-background: #222;
@global-success-background: #32d296;
@global-warning-background: #faa05a;
@global-danger-background: #f0506e;
@global-color: #c8c8c8;
@global-emphasis-color: #ffffff;
@global-muted-color: #9a9a9a;
@global-link-color: #e00000;
@global-link-hover-color: #ff0000;
@global-border: #4a4a4a;
@global-inverse-color: #e8e8e8;
//
// Inverse
//
@inverse-global-color-mode: light;
@inverse-global-color: fade(@global-inverse-color, 70%);
@inverse-global-emphasis-color: @global-inverse-color;
@inverse-global-muted-color: #a8a8a8;
@inverse-global-inverse-color: #1a1a1a;
@inverse-global-primary-background: @global-inverse-color;
@inverse-global-muted-background: fade(@global-inverse-color, 10%);
@inverse-global-border: fade(@global-inverse-color, 20%);
//
// Button
//
@button-default-color: #000000;
@button-primary-color: #000000;
@button-secondary-color: #000000;
@button-text-color: #000000;
@button-text-hover-color: #000000;
@button-text-disabled-color: #000000;
button.uk-button.uk-button-default,
a.uk-button.uk-button-default {
color: @global-color;
}
//
// Component: Navbar
//
@navbar-background: #1a1a1a;
@navbar-nav-item-height: @site-navbar-height;
//
// Off-Canvas
//
@offcanvas-bar-background: #1a1a1a;
//
// Navbar
//
@navbar-dropdown-background: #2a2a2a;
@navbar-dropdown-border: #3a3a3a;
.uk-navbar-dropdown {
border: solid 2px @navbar-dropdown-border;
border-radius: 4px;
.uk-nav-divider {
border-color: @navbar-dropdown-border;
}
}
//
// Form
//
// @internal-form-select-image: "/uikit/images/backgrounds/form-select.svg";
// @internal-form-datalist-image: "/uikit/images/backgrounds/form-datalist.svg";
// @internal-form-radio-image: "/uikit/images/backgrounds/form-radio.svg";
// @internal-form-checkbox-image: "/uikit/images/backgrounds/form-checkbox.svg";
// @internal-form-checkbox-indeterminate-image: "/uikit/images/backgrounds/form-checkbox-indeterminate.svg";
/* Disabled */
.uk-input:disabled,
.uk-select:disabled,
.uk-textarea:disabled {
color: @inverse-global-muted-color;
}

35
client/css/site/uikit-theme.dtp-light.less

@ -0,0 +1,35 @@
//
// Colors
//
@page-background-color: #e8e8e8;
@content-background-color: #ffffff;
@content-border-color: #a8a8a8;
@content-container-color: #c8c8c8;
@site-brand-color: #ff0013;
@button-label-color: #2a2a2a;
@button-label-hover: #ffffff;
@social-link-color: #2a2a2a;
@checkout-button-text-color: #2a2a2a;
@emoji-react-button-active-color: #adc7a0;
@scrollbar-border-color: @content-border-color;
@scrollbar-thumb-color: #ff001380;
//
// Component: Navbar
//
@navbar-nav-item-height: @site-navbar-height;
//
// Form
//
// @internal-form-select-image: "/uikit/images/backgrounds/form-select.svg";
// @internal-form-datalist-image: "/uikit/images/backgrounds/form-datalist.svg";
// @internal-form-radio-image: "/uikit/images/backgrounds/form-radio.svg";
// @internal-form-checkbox-image: "/uikit/images/backgrounds/form-checkbox.svg";
// @internal-form-checkbox-indeterminate-image: "/uikit/images/backgrounds/form-checkbox-indeterminate.svg";

2
client/css/site/variables.less

@ -0,0 +1,2 @@
@brand-color-gab: #00d178;
@site-navbar-height: 64px;

9
webpack.config.js

@ -25,7 +25,7 @@ if (webpackMode === 'development') {
ws: true, ws: true,
}, },
host: 'dev.chat.digitaltelepresence.com', host: 'dev.chat.digitaltelepresence.com',
open: 'external', open: 'local',
port: 3333, port: 3333,
cors: true, cors: true,
ui: { ui: {
@ -49,10 +49,8 @@ if (webpackMode === 'development') {
export default { export default {
entry: { entry: {
'app': './client/js/index.js', 'app': './client/js/index.js',
'chat-client': [ 'chat-light': './client/css/dtp-light.less',
// './client/js/chat-client.js', 'chat-dark': './client/css/dtp-dark.less',
'./client/css/main.less',
],
}, },
devtool: 'source-map', devtool: 'source-map',
mode: webpackMode, mode: webpackMode,
@ -100,6 +98,7 @@ export default {
sourceMap: true, sourceMap: true,
lessOptions: { lessOptions: {
strictMath: false, strictMath: false,
paths: [path.resolve(__dirname, "node_modules")],
}, },
}, },
}, },

Loading…
Cancel
Save