diff --git a/app/models/core-user.js b/app/models/core-user.js new file mode 100644 index 0000000..ab1cf88 --- /dev/null +++ b/app/models/core-user.js @@ -0,0 +1,45 @@ +// core-user.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats'); +const { DTP_THEME_LIST, UserFlagsSchema, UserPermissionsSchema, UserOptInSchema } = require('./lib/user-types'); + +const CoreUserSchema = new Schema({ + created: { type: Date, default: Date.now, required: true, index: -1 }, + updated: { type: Date, default: Date.now, required: true, index: -1 }, + core: { type: Schema.ObjectId, required: true, ref: 'CoreNode' }, + coreUserId: { type: Schema.ObjectId, required: true }, + username: { type: String, required: true }, + username_lc: { type: String, required: true, lowercase: true }, + displayName: { type: String }, + bio: { type: String, maxlength: 300 }, + flags: { type: UserFlagsSchema, select: false }, + permissions: { type: UserPermissionsSchema, select: false }, + optIn: { type: UserOptInSchema, required: true, select: false }, + theme: { type: String, enum: DTP_THEME_LIST, default: 'dtp-light', required: true }, + stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true }, +}); + +CoreUserSchema.index({ + core: 1, + coreUserId: 1, +}, { + unique: true, + name: 'core_userId_unique', +}); + +CoreUserSchema.index({ + core: 1, + username_lc: 1, +}, { + unique: true, + name: 'core_username_lc_unique', +}); + +module.exports = mongoose.model('CoreUser', CoreUserSchema); \ No newline at end of file diff --git a/app/models/lib/user-types.js b/app/models/lib/user-types.js new file mode 100644 index 0000000..d3633a9 --- /dev/null +++ b/app/models/lib/user-types.js @@ -0,0 +1,27 @@ +// user-types.js +// Copyright (C) 2022 DTP Technologies, LLC +// License: Apache-2.0 + +'use strict'; + +const mongoose = require('mongoose'); +const Schema = mongoose.Schema; + +module.exports.DTP_THEME_LIST = ['dtp-light', 'dtp-dark']; + +module.exports.UserFlagsSchema = new Schema({ + isAdmin: { type: Boolean, default: false, required: true }, + isModerator: { type: Boolean, default: false, required: true }, +}); + +module.exports.UserPermissionsSchema = new Schema({ + canLogin: { type: Boolean, default: true, required: true }, + canChat: { type: Boolean, default: true, required: true }, + canComment: { type: Boolean, default: true, required: true }, + canReport: { type: Boolean, default: true, required: true }, +}); + +module.exports.UserOptInSchema = new Schema({ + system: { type: Boolean, default: true, required: true }, + marketing: { type: Boolean, default: true, required: true }, +}); diff --git a/app/models/user.js b/app/models/user.js index d0c0ff9..e7d9a50 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -9,25 +9,7 @@ const mongoose = require('mongoose'); const Schema = mongoose.Schema; const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats'); - -const DTP_THEME_LIST = ['dtp-light', 'dtp-dark']; - -const UserFlagsSchema = new Schema({ - isAdmin: { type: Boolean, default: false, required: true }, - isModerator: { type: Boolean, default: false, required: true }, -}); - -const UserPermissionsSchema = new Schema({ - canLogin: { type: Boolean, default: true, required: true }, - canChat: { type: Boolean, default: true, required: true }, - canComment: { type: Boolean, default: true, required: true }, - canReport: { type: Boolean, default: true, required: true }, -}); - -const UserOptInSchema = new Schema({ - system: { type: Boolean, default: true, required: true }, - marketing: { type: Boolean, default: true, required: true }, -}); +const { DTP_THEME_LIST, UserFlagsSchema, UserPermissionsSchema, UserOptInSchema } = require('./lib/user-types'); const UserSchema = new Schema({ created: { type: Date, default: Date.now, required: true, index: -1 }, diff --git a/app/services/core-node.js b/app/services/core-node.js index 10a2f62..5e24224 100644 --- a/app/services/core-node.js +++ b/app/services/core-node.js @@ -10,8 +10,8 @@ const fetch = require('node-fetch'); // jshint ignore:line const mongoose = require('mongoose'); const CoreNode = mongoose.model('CoreNode'); +const CoreUser = mongoose.model('CoreUser'); const CoreNodeRequest = mongoose.model('CoreNodeRequest'); -const User = mongoose.model('User'); const passport = require('passport'); const OAuth2Strategy = require('passport-oauth2'); @@ -41,6 +41,11 @@ class CoreNodeService extends SiteService { constructor (dtp) { super(dtp, module.exports); + this.populateCoreUser = [ + { + path: 'core' + }, + ]; } async start ( ) { @@ -74,11 +79,20 @@ class CoreNodeService extends SiteService { router.get( coreAuthCallbackUri, (req, res, next) => { - this.log.debug('Core auth callback'); + this.log.debug('Core auth callback', { + strategy: coreAuthStrategyName, + body: req.body, + params: req.params, + query: req.query, + }); return next(); }, passport.authenticate(coreAuthStrategyName, { failureRedirect: '/' }), async (req, res) => { + this.log.info('tagging session as Core', { coreId: core._id }); + req.session.userType = 'Core'; + req.session.coreId = core._id; + // req.login(user, (error) => { // if (error) { // return next(error); @@ -109,19 +123,72 @@ class CoreNodeService extends SiteService { clientSecret: core.oauth.clientSecret, callbackURL, }, - async (accessToken, refreshToken, profile, cb) => { + async (accessToken, refreshToken, params, profile, cb) => { + const NOW = new Date(); this.log.info('Core login', { - name: core.meta.name, - profile, accessToken, refreshToken, + params, + profile, }); - User.findOrCreate({ exampleId: profile.id }, function (err, user) { - return cb(err, user); - }); + try { + const coreUserId = mongoose.Types.ObjectId(params.coreUserId); + const user = await CoreUser.findOneAndUpdate( + { + core: core._id, + coreUserId, + }, + { + $setOnInsert: { + created: NOW, + core: core._id, + coreUserId, + flags: { + isAdmin: false, + isModerator: false, + }, + permissions: { + canLogin: true, + canChat: true, + canComment: true, + canReport: true, + }, + optIn: { + system: true, + marketing: false, + }, + theme: 'dtp-light', + stats: { + uniqueVisitCount: 0, + totalVisitCount: 0, + }, + }, + $set: { + updated: NOW, + username: params.username, + username_lc: params.username_lc, + displayName: params.displayName, + bio: params.bio, + }, + }, + { + upsert: true, + new: true, + }, + ); + return cb(null, user.toObject()); + } catch (error) { + return cb(error); + } }, ); + // const old = coreAuthStrategy._loadUserProfile; + coreAuthStrategy._loadUserProfile = async (accessToken, done) => { + this.log.info('loadUserProfile', { accessToken }); + return done(null); + }; + this.log.info('registering Core auth strategy', { name: coreAuthStrategyName, host: core.address.host, @@ -330,6 +397,15 @@ class CoreNodeService extends SiteService { const cores = await q.lean(); return cores; } + + async getUserByLocalId (userId) { + const user = await CoreUser + .findOne({ _id: userId }) + .select('+flags +permissions +optIn') + .populate(this.populateCoreUser) + .lean(); + return user; + } } module.exports = { diff --git a/app/services/oauth2.js b/app/services/oauth2.js index a1cf6e2..13973d7 100644 --- a/app/services/oauth2.js +++ b/app/services/oauth2.js @@ -85,7 +85,7 @@ class OAuth2Service extends SiteService { this.log.debug('OAuth2 client loaded', { clientID }); return done(null, client); } catch (error) { - this.log.error('failed to deserialize OAuth2 client', { clientId, error }); + this.log.error('failed to deserialize OAuth2 client', { clientID, error }); return done(error); } } diff --git a/app/services/session.js b/app/services/session.js index 60c733c..a4be526 100644 --- a/app/services/session.js +++ b/app/services/session.js @@ -64,13 +64,26 @@ class SessionService extends SiteService { } async serializeUser (user, done) { - return done(null, user._id); + if (user.coreUserId) { + return done(null, `core:${user._id}`); + } + return done(null, `local:${user._id}`); } - async deserializeUser (userId, done) { - const { user: userService } = this.dtp.services; + async deserializeUser (userTag, done) { + const { coreNode: coreNodeService, user: userService } = this.dtp.services; + const [serviceName, userId] = userTag.split(':'); + this.log.debug('load user session', { userTag, serviceName, userId }); try { - const user = await userService.getUserAccount(userId); + let user; + switch (serviceName) { + case 'core': + user = await coreNodeService.getUserByLocalId(userId); + break; + case 'local': + user = await userService.getUserAccount(userId); + break; + } return done(null, user); } catch (error) { this.log.error('failed to deserialize user from session', { error });