Browse Source

Core feature progress

pull/1/head
Rob Colbert 3 years ago
parent
commit
05009ece96
  1. 14
      app/models/core-node.js
  2. 18
      app/models/oauth2-access-token.js
  3. 19
      app/models/oauth2-authorization-code.js
  4. 23
      app/models/oauth2-client.js
  5. 1
      app/models/user.js
  6. 4
      app/services/host-cache.js
  7. 118
      app/services/oauth2.js
  8. 19
      app/services/user.js
  9. 40
      app/views/user/settings.pug
  10. 5
      app/workers/host-services.js
  11. 1
      package.json
  12. 2
      start-local
  13. 5
      yarn.lock

14
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);

18
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);

19
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);

23
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);

1
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' },

4
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 ( ) {

118
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);
const serverOptions = { };
/*
* 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);
this.log.info('creating OAuth2 server instance', { serverOptions });
this.server = oauth2orize.createServer(serverOptions);
/*
* Create OAuth2 server instance
*/
const options = { };
this.log.info('creating OAuth2 server instance', { options });
this.server = oauth2orize.createServer(options);
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 = {

19
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;

40
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,19 +41,27 @@ 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
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
@ -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

5
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

1
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",

2
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

5
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"

Loading…
Cancel
Save