From 05009ece96891d069e5720f6da337aa9d20377dd Mon Sep 17 00:00:00 2001 From: rob Date: Wed, 29 Jun 2022 10:21:15 -0400 Subject: [PATCH] Core feature progress --- app/models/core-node.js | 14 +++ app/models/oauth2-access-token.js | 18 ++++ app/models/oauth2-authorization-code.js | 19 ++++ app/models/oauth2-client.js | 23 +++++ app/models/user.js | 1 + app/services/host-cache.js | 4 +- app/services/oauth2.js | 122 +++++++++++++----------- app/services/user.js | 19 +++- app/views/user/settings.pug | 58 ++++++----- app/workers/host-services.js | 5 +- package.json | 1 + start-local | 2 +- yarn.lock | 5 + 13 files changed, 203 insertions(+), 88 deletions(-) create mode 100644 app/models/oauth2-access-token.js create mode 100644 app/models/oauth2-authorization-code.js create mode 100644 app/models/oauth2-client.js diff --git a/app/models/core-node.js b/app/models/core-node.js index f837683..33f550a 100644 --- a/app/models/core-node.js +++ b/app/models/core-node.js @@ -13,6 +13,20 @@ const CoreNodeSchema = new Schema({ host: { type: String, required: true }, port: { type: Number, min: 1, max: 65535, required: true }, }, + flags: { + isConnected: { type: Boolean, default: false, required: true, index: 1 }, + }, + oauth: { + clientId: { type: String }, + clientSecret: { type: String }, + }, + meta: { + name: { type: String }, + description: { type: String }, + version: { type: String }, + admin: { type: String }, + supportEmail: { type: String }, + }, }); module.exports = mongoose.model('CoreNode', CoreNodeSchema); \ No newline at end of file diff --git a/app/models/oauth2-access-token.js b/app/models/oauth2-access-token.js new file mode 100644 index 0000000..a871ff1 --- /dev/null +++ b/app/models/oauth2-access-token.js @@ -0,0 +1,18 @@ +// oauth2-access-token.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const OAuth2AccessTokenSchema = new Schema({ + token: { type: String, required: true, unique: true, index: 1 }, + user: { type: Schema.ObjectId, required: true, index: 1 }, + clientId: { type: Schema.ObjectId, required: true, index: 1 }, + scope: { type: [String], required: true }, +}); + +module.exports = mongoose.model('OAuth2AccessToken', OAuth2AccessTokenSchema); \ No newline at end of file diff --git a/app/models/oauth2-authorization-code.js b/app/models/oauth2-authorization-code.js new file mode 100644 index 0000000..86ab806 --- /dev/null +++ b/app/models/oauth2-authorization-code.js @@ -0,0 +1,19 @@ +// oauth2-authorization-code.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const OAuth2AuthorizationCodeSchema = new Schema({ + code: { type: String, required: true, index: 1 }, + clientId: { type: Schema.ObjectId, required: true, index: 1 }, + redirectURI: { type: String, required: true }, + user: { type: Schema.ObjectId, required: true, index: 1 }, + scope: { type: [String], required: true }, +}); + +module.exports = mongoose.model('OAuth2AuthorizationCode', OAuth2AuthorizationCodeSchema); \ No newline at end of file diff --git a/app/models/oauth2-client.js b/app/models/oauth2-client.js new file mode 100644 index 0000000..e3f24aa --- /dev/null +++ b/app/models/oauth2-client.js @@ -0,0 +1,23 @@ +// oauth2-client.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const OAuth2ClientSchema = new Schema({ + created: { type: Date, default: Date.now, required: true }, + updated: { type: Date, default: Date.now, required: true }, + node: { + owner: { type: Schema.ObjectId, required: true, index: 1 }, + name: { type: String, required: true }, + domain: { type: String, required: true, unique: true }, + }, + secret: { type: String, required: true }, + redirectURI: { type: String, required: true }, +}); + +module.exports = mongoose.model('OAuth2Client', OAuth2ClientSchema); \ No newline at end of file diff --git a/app/models/user.js b/app/models/user.js index 14d681b..6e34e21 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -37,6 +37,7 @@ const UserSchema = new Schema({ passwordSalt: { type: String, required: true }, password: { type: String, required: true }, displayName: { type: String }, + bio: { type: String }, picture: { large: { type: Schema.ObjectId, ref: 'Image' }, small: { type: Schema.ObjectId, ref: 'Image' }, diff --git a/app/services/host-cache.js b/app/services/host-cache.js index ae60cd4..f2348ec 100644 --- a/app/services/host-cache.js +++ b/app/services/host-cache.js @@ -25,7 +25,9 @@ class HostCacheService extends SiteService { this.log.info('connecting UDP host-cache socket'); this.hostCache.bind(0, '127.0.0.1'); - this.hostCache.connect(8000, '127.0.0.1'); + + const HOST_PORT = parseInt(process.env.DTP_HOST_CACHE_PORT || '8000', 10); + this.hostCache.connect(HOST_PORT, '127.0.0.1'); } async stop ( ) { diff --git a/app/services/oauth2.js b/app/services/oauth2.js index c6605b9..90beeb0 100644 --- a/app/services/oauth2.js +++ b/app/services/oauth2.js @@ -4,13 +4,18 @@ 'use strict'; -const passport = require('passport'); - const mongoose = require('mongoose'); -const Schema = mongoose.Schema; + +const OAuth2Client = mongoose.model('OAuth2Client'); +const OAuth2AuthorizationCode = mongoose.model('OAuth2AuthorizationCode'); +const OAuth2AccessToken = mongoose.model('OAuth2AccessToken'); const uuidv4 = require('uuid').v4; +const striptags = require('striptags'); + const oauth2orize = require('oauth2orize'); +const passport = require('passport'); +const generatePassword = require('password-generator'); const { SiteService/*, SiteError*/ } = require('../../lib/site-lib'); @@ -21,69 +26,32 @@ class OAuth2Service extends SiteService { } async start ( ) { - this.models = { }; - - /* - * OAuth2Client Model - */ - const ClientSchema = new Schema({ - created: { type: Date, default: Date.now, required: true }, - updated: { type: Date, default: Date.now, required: true }, - secret: { type: String, required: true }, - redirectURI: { type: String, required: true }, - }); - this.log.info('registering OAuth2Client model'); - this.models.Client = mongoose.model('OAuth2Client', ClientSchema); - - /* - * OAuth2AuthorizationCode model - */ - const AuthorizationCodeSchema = new Schema({ - code: { type: String, required: true, index: 1 }, - clientId: { type: Schema.ObjectId, required: true, index: 1 }, - redirectURI: { type: String, required: true }, - user: { type: Schema.ObjectId, required: true, index: 1 }, - scope: { type: [String], required: true }, - }); - this.log.info('registering OAuth2AuthorizationCode model'); - this.models.AuthorizationCode = mongoose.model('OAuth2AuthorizationCode', AuthorizationCodeSchema); - - /* - * OAuth2AccessToken model - */ - const AccessTokenSchema = new Schema({ - token: { type: String, required: true, unique: true, index: 1 }, - user: { type: Schema.ObjectId, required: true, index: 1 }, - clientId: { type: Schema.ObjectId, required: true, index: 1 }, - scope: { type: [String], required: true }, - }); - this.log.info('registering OAuth2AccessToken model'); - this.models.AccessToken = mongoose.model('OAuth2AccessToken', AccessTokenSchema); - - /* - * Create OAuth2 server instance - */ - const options = { }; - this.log.info('creating OAuth2 server instance', { options }); - this.server = oauth2orize.createServer(options); + const serverOptions = { }; + + this.log.info('creating OAuth2 server instance', { serverOptions }); + this.server = oauth2orize.createServer(serverOptions); + + this.log.info('registering OAuth2 action handlers'); this.server.grant(oauth2orize.grant.code(this.processGrant.bind(this))); this.server.exchange(oauth2orize.exchange.code(this.processExchange.bind(this))); - /* - * Register client serialization callbacks - */ - this.log.info('registering OAuth2 client serialization routines'); + this.log.info('registering OAuth2 serialization routines'); this.server.serializeClient(this.serializeClient.bind(this)); this.server.deserializeClient(this.deserializeClient.bind(this)); } async serializeClient (client, done) { + this.log.debug('serializeClient', { client }); return done(null, client.id); } async deserializeClient (clientId, done) { + this.log.debug('deserializeClient', { clientId }); try { - const client = await this.models.Client.findOne({ _id: clientId }).lean(); + const client = await OAuth2Client + .findOne({ _id: clientId }) + .lean(); + this.log.debug('OAuth2 client loaded', { clientId, client }); return done(null, client); } catch (error) { this.log.error('failed to deserialize OAuth2 client', { clientId, error }); @@ -125,7 +93,7 @@ class OAuth2Service extends SiteService { async processAuthorize (clientID, redirectURI, done) { try { - const client = await this.models.Clients.findOne({ clientID }); + const client = await OAuth2Client.findOne({ clientID }); if (!client) { return done(null, false); } @@ -142,7 +110,7 @@ class OAuth2Service extends SiteService { async processGrant (client, redirectURI, user, ares, done) { try { var code = uuidv4(); - var ac = new this.models.AuthorizationCode({ + var ac = new OAuth2AuthorizationCode({ code, clientId: client.id, redirectURI, @@ -159,7 +127,7 @@ class OAuth2Service extends SiteService { async processExchange (client, code, redirectURI, done) { try { - const ac = await this.models.AuthorizationCode.findOne({ code }); + const ac = await OAuth2AuthorizationCode.findOne({ code }); if (client.id !== ac.clientId) { return done(null, false); } @@ -168,7 +136,7 @@ class OAuth2Service extends SiteService { } var token = uuidv4(); - var at = new this.models.AccessToken({ + var at = new OAuth2AccessToken({ token, user: ac.userId, clientId: ac.clientId, @@ -182,6 +150,46 @@ class OAuth2Service extends SiteService { return done(error); } } + + /** + * Creates a new OAuth2 client, and generates a Client ID and Secret for it. + * @param {User} user The authenticated user issuing the request to create an + * "app" for use when calling DTP APIs. + * @param {Document} clientDefinition The definition of the client to be + * created including the name and domain of the node. + * @returns new client instance with valid _id. + */ + async createClient (user, clientDefinition) { + const NOW = new Date(); + const PASSWORD_LEN = parseInt(process.env.DTP_CORE_AUTH_PASSWORD_LEN || '64', 10); + + const client = new OAuth2Client(); + client.created = NOW; + client.updated = NOW; + client.node.owner = user._id; + client.node.name = striptags(clientDefinition.name.trim()); + client.node.domain = striptags(clientDefinition.domain.trim().toLowerCase()); + client.user = user._id; + client.secret = generatePassword(PASSWORD_LEN, false); + client.redirectURI = clientDefinition.redirectURI; + await client.save(); + + this.log.info('new OAuth2 client created', { + clientId: client._id, + owner: user._id, + node: client.node.name, + domain: client.node.domain, + }); + + return client.toObject(); + } + + async getClientById (clientId) { + const client = await OAuth2Client + .findOne({ _id: clientId }) + .lean(); + return client; + } } module.exports = { diff --git a/app/services/user.js b/app/services/user.js index e78c7e5..c475503 100644 --- a/app/services/user.js +++ b/app/services/user.js @@ -249,6 +249,7 @@ 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( @@ -258,6 +259,7 @@ class UserService { username: userDefinition.username, username_lc, displayName: userDefinition.displayName, + bio: userDefinition.bio, theme: userDefinition.theme || 'dtp-light', }, }, @@ -383,13 +385,24 @@ class UserService { } filterUserObject (user) { - return { + const filteredUser = { _id: user._id, - email: user.email, 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 getUserAccount (userId) { @@ -442,7 +455,7 @@ class UserService { username = username.trim().toLowerCase(); const user = await User .findOne({ username_lc: username }) - .select('_id created username username_lc displayName picture header') + .select('_id created username username_lc displayName bio picture header') .populate(this.populateUser) .lean(); return user; diff --git a/app/views/user/settings.pug b/app/views/user/settings.pug index 67c9281..8d59ab7 100644 --- a/app/views/user/settings.pug +++ b/app/views/user/settings.pug @@ -9,12 +9,14 @@ block content section.uk-section.uk-section-default.uk-section-small .uk-container + pre= JSON.stringify(userProfile, null, 2) + div(uk-grid) div(class="uk-width-1-1 uk-width-1-3@m") - var currentProfile = null; - if (user.picture && user.picture.large) { - currentProfile = user.picture.large; + if (userProfile.picture && userProfile.picture.large) { + currentProfile = userProfile.picture.large; } .uk-margin .uk-card.uk-card-default.uk-card-small @@ -22,7 +24,7 @@ block content h1.uk-card-title Profile Photo .uk-card-body +renderFileUploadImage( - `/user/${user._id}/profile-photo`, + `/user/${userProfile._id}/profile-photo`, 'profile-picture-upload', 'profile-picture-file', 'site-profile-picture', @@ -39,30 +41,38 @@ block content p Enabling Two-Factor Authentication (2FA) with a One-Time Password authenticator app can help protect your account and settings from unauthorized access. .uk-card-footer.uk-text-center if hasOtpAccount - a(href=`/user/${user._id}/otp-disable`).uk-button.dtp-button-danger Disable 2FA + a(href=`/user/${userProfile._id}/otp-disable`).uk-button.dtp-button-danger Disable 2FA else - a(href=`/user/${user._id}/otp-setup`).uk-button.dtp-button-default Enable 2FA + a(href=`/user/${userProfile._id}/otp-setup`).uk-button.dtp-button-default Enable 2FA div(class="uk-width-1-1 uk-width-expand@m") h1 Settings - form(method="POST", action=`/user/${user._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form + form(method="POST", action=`/user/${userProfile._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form .uk-margin label(for="username").uk-form-label Username - input(id="username", name="username", type="text", placeholder="Enter username", value= user.username).uk-input + 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= user.displayName).uk-input + input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= userProfile.displayName).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 + label(for="bio").uk-form-label Bio + textarea(id="bio", name="bio", rows="4", placeholder="Enter bio info").uk-textarea= userProfile.bio + + fieldset + legend Security + + .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 fieldset legend Email Preferences @@ -70,13 +80,13 @@ block content .uk-margin label(for="email").uk-form-label span Email Address - if user.flags.isEmailVerified + if userProfile.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= user.email).uk-input - if user.flags.isEmailVerified + input(id="email", name="email", type="email", placeholder="Enter email address", value= userProfile.email).uk-input + if userProfile.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! @@ -86,17 +96,17 @@ block content .uk-margin div(uk-grid).uk-grid-small label - input(id="optin-system", name="optIn.system", type="checkbox", checked= user.optIn ? user.optIn.system : false).uk-checkbox + input(id="optin-system", name="optIn.system", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.system : false).uk-checkbox | System messages label - input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= user.optIn ? user.optIn.marketing : false).uk-checkbox + input(id="optin-marketing", name="optIn.marketing", type="checkbox", checked= userProfile.optIn ? userProfile.optIn.marketing : false).uk-checkbox | Sales / Marketing .uk-margin label(for="theme").uk-form-label UI Theme select(id="theme", name="theme").uk-select - option(value="dtp-light", selected= user ? user.theme === 'dtp-light' : true) Light Mode - option(value="dtp-dark", selected= user ? user.theme === 'dtp-dark' : false) Dark Mode + option(value="dtp-light", selected= user ? userProfile.theme === 'dtp-light' : true) Light Mode + option(value="dtp-dark", selected= user ? userProfile.theme === 'dtp-dark' : false) Dark Mode .uk-margin button(type="submit").uk-button.dtp-button-primary Update account settings \ No newline at end of file diff --git a/app/workers/host-services.js b/app/workers/host-services.js index 99cb011..6326632 100644 --- a/app/workers/host-services.js +++ b/app/workers/host-services.js @@ -623,10 +623,11 @@ module.expireNetHosts = async ( ) => { /* * Host Cache server socket setup */ + const HOST_PORT = parseInt(process.env.DTP_HOST_CACHE_PORT || '8000', 10); module.log.info('creating server UDP socket'); module.server = dgram.createSocket('udp4', module.onHostCacheMessage); - module.log.info('binding server UDP socket'); - module.server.bind(8000); + module.log.info('binding server UDP socket', { port: HOST_PORT }); + module.server.bind(HOST_PORT); /* * Site Platform startup diff --git a/package.json b/package.json index 59c21dd..961b234 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "passport": "^0.5.2", "passport-local": "^1.0.0", "passport-oauth2": "^1.6.1", + "password-generator": "^2.3.2", "pug": "^3.0.2", "qrcode": "^1.5.0", "rate-limiter-flexible": "^2.3.6", diff --git a/start-local b/start-local index 5f17397..4565df4 100755 --- a/start-local +++ b/start-local @@ -2,7 +2,7 @@ MINIO_CI_CD=1 -MINIO_ROOT_USER="webapp" +MINIO_ROOT_USER="dtp-base" MINIO_ROOT_PASSWORD="302888b9-c3d8-40f5-92de-6a3c57186af5" export MINIO_ROOT_USER MINIO_ROOT_PASSWORD MINIO_CI_CD diff --git a/yarn.lock b/yarn.lock index cd861f8..f9f989f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6378,6 +6378,11 @@ passport@^0.5.2: passport-strategy "1.x.x" pause "0.0.1" +password-generator@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/password-generator/-/password-generator-2.3.2.tgz#9626f778d64d26f7c2f73b64389407e28f62eecd" + integrity sha512-kJWUrdveSAqHCeWJWnv5vNc89hFHM5au+pvKja5+xCTxlRF3zQaecJlR6hSoOotAJtQ3otQq4/Q4iWc/TxsXhA== + path-dirname@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"