Compare commits
7 Commits
cc7b07dcec
...
d5fac1c9ba
Author | SHA1 | Date | |
---|---|---|---|
d5fac1c9ba | |||
be12b8a5cd | |||
4ace7a3ea0 | |||
3d79aafad9 | |||
820e05ba2a | |||
e4ee428a14 | |||
f5604e4a46 |
12
options.json
12
options.json
@ -11,7 +11,16 @@
|
|||||||
},
|
},
|
||||||
"validUserTypes": ["user", "service", "system"],
|
"validUserTypes": ["user", "service", "system"],
|
||||||
"registrationEnabled": true,
|
"registrationEnabled": true,
|
||||||
"requireCodeForRegister": true
|
"requireCodeForRegister": true,
|
||||||
|
"OAuthProviders": [
|
||||||
|
{
|
||||||
|
"name": "Discord",
|
||||||
|
"authoriseURL": "https://discord.com/oauth2/authorize",
|
||||||
|
"tokenURL": "https://discord.com/api/v10/oauth2/token",
|
||||||
|
"profileURL": "https://discord.com/api/v10/users/@me",
|
||||||
|
"iconURL": "https://assets-global.website-files.com/6257adef93867e50d84d30e2/636e0a69f118df70ad7828d4_icon_clyde_blurple_RGB.svg"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"logger": {
|
"logger": {
|
||||||
"customTypes": ["access", "unauthorised"],
|
"customTypes": ["access", "unauthorised"],
|
||||||
@ -38,7 +47,6 @@
|
|||||||
"mongodb": {
|
"mongodb": {
|
||||||
"load": true,
|
"load": true,
|
||||||
"client": {
|
"client": {
|
||||||
"useNewUrlParser": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,5 +34,8 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node index.js"
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ class Server extends EventEmitter {
|
|||||||
this.registrationEnabled = options.registrationEnabled;
|
this.registrationEnabled = options.registrationEnabled;
|
||||||
// Only lets people with valid registration urls to register
|
// Only lets people with valid registration urls to register
|
||||||
this.requireCodeForRegister = options.requireCodeForRegister;
|
this.requireCodeForRegister = options.requireCodeForRegister;
|
||||||
|
this.OAuthProviders = options.OAuthProviders;
|
||||||
|
|
||||||
this.server = null;
|
this.server = null;
|
||||||
this.app = express();
|
this.app = express();
|
||||||
@ -194,10 +195,15 @@ class Server extends EventEmitter {
|
|||||||
|
|
||||||
const { DISCORD_ID, DISCORD_SECRET } = process.env;
|
const { DISCORD_ID, DISCORD_SECRET } = process.env;
|
||||||
const { callbackURL, discord } = this._options;
|
const { callbackURL, discord } = this._options;
|
||||||
|
const authParams = {
|
||||||
|
clientID: DISCORD_ID,
|
||||||
|
clientSecret: DISCORD_SECRET,
|
||||||
|
callbackURL: this.baseURL + callbackURL + '/discord',
|
||||||
|
scope: discord.scope,
|
||||||
|
version: discord.version
|
||||||
|
};
|
||||||
|
|
||||||
this.authenticator.addStrategy('discord', new DiscordStrategy({
|
this.authenticator.addStrategy('discord', new DiscordStrategy(authParams, async (accessToken, refreshToken, profile, callback) => {
|
||||||
clientID: DISCORD_ID, clientSecret: DISCORD_SECRET, callbackURL: this.baseURL + callbackURL + '/discord', scope: discord.scope, version: discord.version
|
|
||||||
}, async (accessToken, refreshToken, profile, callback) => {
|
|
||||||
this.logger.info(`${profile.username} (${profile.id}) is logging in.`);
|
this.logger.info(`${profile.username} (${profile.id}) is logging in.`);
|
||||||
// We don't need this here
|
// We don't need this here
|
||||||
delete profile.accessToken;
|
delete profile.accessToken;
|
||||||
@ -205,7 +211,7 @@ class Server extends EventEmitter {
|
|||||||
let err = null;
|
let err = null;
|
||||||
if (!user) err = new Error('No such user');
|
if (!user) err = new Error('No such user');
|
||||||
callback(err, user);
|
callback(err, user);
|
||||||
}), { successRedirect: '/home' });
|
}), { successRedirect: '/home', authParams });
|
||||||
|
|
||||||
this.authenticator.addStrategy('local', new LocalStrategy(async (username, password, callback) => {
|
this.authenticator.addStrategy('local', new LocalStrategy(async (username, password, callback) => {
|
||||||
const user = await this.userDatabase.findUser(username);
|
const user = await this.userDatabase.findUser(username);
|
||||||
@ -222,6 +228,16 @@ class Server extends EventEmitter {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get clientSettings () {
|
||||||
|
return {
|
||||||
|
registrationEnabled: this.registrationEnabled,
|
||||||
|
requireCodeForRegister: this.requireCodeForRegister,
|
||||||
|
OAuthProviders: this.OAuthProviders.map(provider => {
|
||||||
|
return { name: provider.name, icon: provider.iconURL, url: `/api/user/connect/${provider.name.toLowerCase()}` };
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.once('message', (msg) => {
|
process.once('message', (msg) => {
|
||||||
|
@ -21,7 +21,7 @@ class Registry {
|
|||||||
this.endpoints.forEach(ep => {
|
this.endpoints.forEach(ep => {
|
||||||
let str = `${spacer}[${ep.resolveable}]\n${spacer}[${ep.methods.map(([ method ]) => method.toUpperCase()).join(', ') || 'NONE'}] ${ep.path}`;
|
let str = `${spacer}[${ep.resolveable}]\n${spacer}[${ep.methods.map(([ method ]) => method.toUpperCase()).join(', ') || 'NONE'}] ${ep.path}`;
|
||||||
|
|
||||||
for (const [ sub, method ] of ep.subpaths) str += `\n${spacer.repeat(2)} - ${method.toUpperCase()}: ${sub}`;
|
for (const [ method, sub ] of ep.subpaths) str += `\n${spacer.repeat(2)} - ${method.toUpperCase()}: ${sub}`;
|
||||||
out.push(str);
|
out.push(str);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ class MongoDB {
|
|||||||
this.logger.status('Initializing database connection.');
|
this.logger.status('Initializing database connection.');
|
||||||
|
|
||||||
await this.client.connect();
|
await this.client.connect();
|
||||||
|
this.logger.debug(`Connected, selecting DB`);
|
||||||
this.db = await this.client.db(this.database);
|
this.db = await this.client.db(this.database);
|
||||||
|
|
||||||
this.logger.status('Database connected.');
|
this.logger.status('Database connected.');
|
||||||
|
@ -10,7 +10,7 @@ class Api404 extends ApiEndpoint {
|
|||||||
super(server, {
|
super(server, {
|
||||||
name: 'api404',
|
name: 'api404',
|
||||||
path: '/*',
|
path: '/*',
|
||||||
loadOrder: 10
|
loadOrder: 9
|
||||||
});
|
});
|
||||||
|
|
||||||
this.methods = [
|
this.methods = [
|
||||||
|
@ -14,9 +14,9 @@ class LoginEndpoint extends ApiEndpoint {
|
|||||||
[ 'post', this.twoFactorRedirect.bind(this), [ server.authenticator.local ]]
|
[ 'post', this.twoFactorRedirect.bind(this), [ server.authenticator.local ]]
|
||||||
];
|
];
|
||||||
this.subpaths = [
|
this.subpaths = [
|
||||||
[ '/fail', 'get', this.fail.bind(this) ],
|
[ 'get', '/fail', this.fail.bind(this) ],
|
||||||
[ '/discord', 'get', server.authenticator.discord ],
|
[ 'get', '/discord', server.authenticator.discord ],
|
||||||
[ '/verify', 'post', this.twoFactor.bind(this), server.authenticator.authenticate ], // 2FA verification
|
[ 'post', '/verify', this.twoFactor.bind(this), server.authenticator.authenticate ], // 2FA verification
|
||||||
];
|
];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,9 +13,9 @@ class RegisterEndpoint extends ApiEndpoint {
|
|||||||
[ 'post', this.register.bind(this), [ this.notLoggedIn.bind(this) ]]
|
[ 'post', this.register.bind(this), [ this.notLoggedIn.bind(this) ]]
|
||||||
];
|
];
|
||||||
this.subpaths = [
|
this.subpaths = [
|
||||||
[ '/finalise', 'post', this.finaliseRegistration.bind(this), [ server.auth.authenticate ]],
|
[ 'post', '/finalise', this.finaliseRegistration.bind(this), [ server.auth.authenticate ]],
|
||||||
[ '/toggle', 'post', this.toggleRegistration.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]],
|
[ 'post', '/toggle', this.toggleRegistration.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]],
|
||||||
[ '/code', 'get', this.registrationCode.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]]
|
[ 'get', '/code', this.registrationCode.bind(this), [ server.auth.createAuthoriser('administrator', 5) ]]
|
||||||
];
|
];
|
||||||
this.middleware = [ ];
|
this.middleware = [ ];
|
||||||
|
|
||||||
|
25
src/server/endpoints/api/Settings.js
Normal file
25
src/server/endpoints/api/Settings.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
const { ApiEndpoint } = require("../../interfaces");
|
||||||
|
|
||||||
|
class SettingsEndpoint extends ApiEndpoint {
|
||||||
|
|
||||||
|
constructor (server) {
|
||||||
|
super(server, {
|
||||||
|
name: 'settings',
|
||||||
|
path: '/settings'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.methods = [
|
||||||
|
[ 'get', this.getSettings.bind(this) ]
|
||||||
|
];
|
||||||
|
|
||||||
|
// this.middleware = [ server.auth.authenticate ];
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getSettings (req, res) {
|
||||||
|
res.json(this.server.clientSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = SettingsEndpoint;
|
@ -1,10 +1,17 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
const { ApiEndpoint } = require("../../interfaces");
|
const { ApiEndpoint } = require("../../interfaces");
|
||||||
const { UtilMiddleware } = require('../../middleware');
|
const { UtilMiddleware } = require('../../middleware');
|
||||||
const pkg = require('../../../../package.json');
|
const pkg = require('../../../../package.json');
|
||||||
|
const { Util } = require("../../../util");
|
||||||
|
|
||||||
const qrcode = require('qrcode');
|
const qrcode = require('qrcode');
|
||||||
const { authenticator } = require('otplib');
|
const { authenticator } = require('otplib');
|
||||||
|
|
||||||
|
const { inspect } = require('node:util');
|
||||||
|
|
||||||
|
// Populated by the constructor
|
||||||
|
const ServiceProfileLinks = {};
|
||||||
|
|
||||||
class UserEndpoint extends ApiEndpoint {
|
class UserEndpoint extends ApiEndpoint {
|
||||||
|
|
||||||
constructor (server) {
|
constructor (server) {
|
||||||
@ -19,11 +26,18 @@ class UserEndpoint extends ApiEndpoint {
|
|||||||
];
|
];
|
||||||
|
|
||||||
this.subpaths = [
|
this.subpaths = [
|
||||||
[ '/2fa', 'get', this.twoFactor.bind(this) ],
|
// 2 Factor Authentication
|
||||||
[ '/2fa/disable', 'get', this.disable2fa.bind(this) ],
|
[ 'get', '/2fa', this.twoFactor.bind(this) ],
|
||||||
[ '/2fa/verify', 'post', this.verify2fa.bind(this) ],
|
[ 'post', '/2fa/disable', this.disable2fa.bind(this) ],
|
||||||
[ '/applications', 'get', this.applications.bind(this) ],
|
[ 'post', '/2fa/verify', this.verify2fa.bind(this) ],
|
||||||
[ '/applications', 'post', this.createApplication.bind(this) ],
|
|
||||||
|
// 3rd Party Connections
|
||||||
|
[ 'get', '/connect/:service', this.connectOAuth.bind(this) ],
|
||||||
|
[ 'get', '/connect/:service/finalise', this.connectOAuthFinalise.bind(this) ],
|
||||||
|
|
||||||
|
// Applications
|
||||||
|
[ 'get', '/applications', this.applications.bind(this) ],
|
||||||
|
[ 'post', '/applications', this.createApplication.bind(this) ],
|
||||||
];
|
];
|
||||||
|
|
||||||
this.middleware = [
|
this.middleware = [
|
||||||
@ -31,27 +45,16 @@ class UserEndpoint extends ApiEndpoint {
|
|||||||
UtilMiddleware.requireBody
|
UtilMiddleware.requireBody
|
||||||
];
|
];
|
||||||
|
|
||||||
|
server.OAuthProviders.forEach(provider => {
|
||||||
|
ServiceProfileLinks[provider.name.toLowerCase()] = provider.profileURL;
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
user (req, res) {
|
user (req, res) {
|
||||||
return res.json(req.user.safeJson);
|
return res.json(req.user.safeJson);
|
||||||
}
|
}
|
||||||
|
|
||||||
async applications (req, res) {
|
|
||||||
const { user } = req;
|
|
||||||
const applications = await user.fetchApplications();
|
|
||||||
res.json(Object.values(applications).map(app => app.safeJson));
|
|
||||||
}
|
|
||||||
|
|
||||||
async createApplication (req, res) {
|
|
||||||
const { body, user } = req;
|
|
||||||
const { name } = body;
|
|
||||||
if (!name) return res.status(400).send('Missing name');
|
|
||||||
const application = await user.createApplication(name);
|
|
||||||
res.json(application.safeJson);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
async twoFactor (req, res) {
|
async twoFactor (req, res) {
|
||||||
|
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
@ -101,6 +104,84 @@ class UserEndpoint extends ApiEndpoint {
|
|||||||
res.send('Disabled 2FA');
|
res.send('Disabled 2FA');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectOAuth (req, res) {
|
||||||
|
const { params } = req;
|
||||||
|
const service = params.service.toLowerCase();
|
||||||
|
if (!this.server.OAuthProviders.some(provider => provider.name.toLowerCase() === service)) return res.status(404).send('Provider not found');
|
||||||
|
|
||||||
|
const authoriseLink = this.server.auth.getOAuthLink(service, { redirect_uri: this.redirectURI(service) });
|
||||||
|
res.redirect(authoriseLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
async connectOAuthFinalise (req, res) {
|
||||||
|
const { params, query, user } = req;
|
||||||
|
const service = params.service.toLowerCase();
|
||||||
|
if (!this.server.OAuthProviders.some(provider => provider.name.toLowerCase() === service)) return res.status(404).send('Provider not found');
|
||||||
|
const { code } = query;
|
||||||
|
|
||||||
|
const OAuthParams = this.server.auth.getOAuthTokenParams(service);
|
||||||
|
const tokenLink = OAuthParams.link;
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
client_id: OAuthParams.clientID,
|
||||||
|
client_secret: OAuthParams.clientSecret,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: this.redirectURI(service),
|
||||||
|
code
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await Util.post(tokenLink, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 400) return res.status(400).send('Invalid code');
|
||||||
|
if (response.status === 401) return res.status(401).end();
|
||||||
|
if (!response.success) {
|
||||||
|
this.logger.error(`Error in OAuth: ${response.status} ${inspect(response.data)}`);
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceProfileLink = ServiceProfileLinks[service];
|
||||||
|
const profile = await Util.get(serviceProfileLink, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${response.data.access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (profile.status !== 200) {
|
||||||
|
this.logger.error(`Non 200 code while fetching profile: ${profile.status} ${inspect(profile.data)}`);
|
||||||
|
return res.status(500).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
user.addExternalProfile(service, profile.data);
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.send(`Successfully linked ${user.username} with ${profile.data.username} (${profile.data.id})`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async applications (req, res) {
|
||||||
|
const { user } = req;
|
||||||
|
const applications = await user.fetchApplications();
|
||||||
|
res.json(Object.values(applications).map(app => app.safeJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createApplication (req, res) {
|
||||||
|
const { body, user } = req;
|
||||||
|
const { name } = body;
|
||||||
|
if (!name) return res.status(400).send('Missing name');
|
||||||
|
const application = await user.createApplication(name);
|
||||||
|
res.json(application.safeJson);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectURI (service) {
|
||||||
|
return encodeURI(`${this.server.baseURL}/api/user/connect/${service}/finalise`);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = UserEndpoint;
|
module.exports = UserEndpoint;
|
@ -11,9 +11,9 @@ class UsersEndpoint extends ApiEndpoint {
|
|||||||
|
|
||||||
this.methods.push([ 'get', this.getUsers.bind(this), [ server.auth.createAuthoriser('administrator', 10) ]]);
|
this.methods.push([ 'get', this.getUsers.bind(this), [ server.auth.createAuthoriser('administrator', 10) ]]);
|
||||||
this.subpaths = [
|
this.subpaths = [
|
||||||
[ '/:userid', 'get', this.user.bind(this), [ server.auth.createAuthoriser('administrator', 10) ]],
|
[ 'get', '/:userid', this.user.bind(this), [ server.auth.createAuthoriser('administrator', 10) ]],
|
||||||
[ '/:userid/avatar', 'get', this.avatar.bind(this) ],
|
[ 'get', '/:userid/avatar', this.avatar.bind(this) ],
|
||||||
[ '/:userid/applications', 'get', this.userApplications.bind(this), [ server.auth.createAuthoriser('administrator', 10) ]]
|
[ 'get', '/:userid/applications', this.userApplications.bind(this), [ server.auth.createAuthoriser('administrator', 10) ]]
|
||||||
];
|
];
|
||||||
// this.middleware = [ server.auth.createAuthoriser('administrator', 10) ];
|
// this.middleware = [ server.auth.createAuthoriser('administrator', 10) ];
|
||||||
|
|
||||||
|
@ -8,7 +8,8 @@ class Home extends Endpoint {
|
|||||||
|
|
||||||
super(server, {
|
super(server, {
|
||||||
name: 'home',
|
name: 'home',
|
||||||
path: '/*'
|
path: '/*',
|
||||||
|
loadOrder: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
this.methods = [
|
this.methods = [
|
||||||
|
@ -15,7 +15,7 @@ class Endpoint {
|
|||||||
|
|
||||||
// Subpaths that should exist on *all* endpoints, the subpaths property can be overwritten, so storing these separately to ensure they exist
|
// Subpaths that should exist on *all* endpoints, the subpaths property can be overwritten, so storing these separately to ensure they exist
|
||||||
this._subpaths = [
|
this._subpaths = [
|
||||||
[ '/debug', 'post', this.toggleDebug.bind(this), [ server.authenticator.createAuthoriser('developer') ]]
|
[ 'post', '/debug', this.toggleDebug.bind(this), [ server.authenticator.createAuthoriser('developer') ]]
|
||||||
];
|
];
|
||||||
|
|
||||||
this.middleware = [];
|
this.middleware = [];
|
||||||
@ -44,7 +44,7 @@ class Endpoint {
|
|||||||
|
|
||||||
this.subpaths = [ ...this._subpaths, ...this.subpaths ];
|
this.subpaths = [ ...this._subpaths, ...this.subpaths ];
|
||||||
// eslint-disable-next-line prefer-const
|
// eslint-disable-next-line prefer-const
|
||||||
for (let [ sub, method, cb, mw = [] ] of this.subpaths) {
|
for (let [ method, sub, cb, mw = [] ] of this.subpaths) {
|
||||||
if (typeof method !== 'string') throw new Error(`Invalid method parameter type in Endpoint ${this.name} subpath ${sub}`);
|
if (typeof method !== 'string') throw new Error(`Invalid method parameter type in Endpoint ${this.name} subpath ${sub}`);
|
||||||
if (!this.middleware.length && !mw.length && !cb) throw new Error(`Cannot have endpoint with no handler and no middleware, expecting at least one to be defined`);
|
if (!this.middleware.length && !mw.length && !cb) throw new Error(`Cannot have endpoint with no handler and no middleware, expecting at least one to be defined`);
|
||||||
if (typeof mw === 'function') mw = [ mw ];
|
if (typeof mw === 'function') mw = [ mw ];
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable camelcase */
|
||||||
const { AbstractUserDatabase } = require('../interfaces');
|
const { AbstractUserDatabase } = require('../interfaces');
|
||||||
const { Util } = require('../../util');
|
const { Util } = require('../../util');
|
||||||
|
|
||||||
@ -5,6 +6,8 @@ const session = require('express-session');
|
|||||||
const MongoStore = require('connect-mongo');
|
const MongoStore = require('connect-mongo');
|
||||||
const Passport = require('passport');
|
const Passport = require('passport');
|
||||||
|
|
||||||
|
const OAuthLinks = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes care of sessions and authentication
|
* Takes care of sessions and authentication
|
||||||
*
|
*
|
||||||
@ -61,6 +64,14 @@ class Authenticator {
|
|||||||
callback(null, user);
|
callback(null, user);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Stores parameters for OAuth requests, such as client id, secret, scope etc
|
||||||
|
this.OAuthParams = {};
|
||||||
|
server.OAuthProviders.forEach(provider => {
|
||||||
|
const providerName = provider.name.toLowerCase();
|
||||||
|
OAuthLinks[providerName] = provider.authoriseURL;
|
||||||
|
OAuthLinks[providerName +'Token'] = provider.tokenURL;
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -71,9 +82,10 @@ class Authenticator {
|
|||||||
* @param {object} [{ failureRedirect = '/api/login/fail', successRedirect = '/home' }={}]
|
* @param {object} [{ failureRedirect = '/api/login/fail', successRedirect = '/home' }={}]
|
||||||
* @memberof Authenticator
|
* @memberof Authenticator
|
||||||
*/
|
*/
|
||||||
addStrategy (name, strategy, { failureRedirect = '/api/login/fail', successRedirect } = {}) {
|
addStrategy (name, strategy, { failureRedirect = '/api/login/fail', successRedirect, authParams } = {}) {
|
||||||
this.logger.info(`Adding ${name} authentication strategy`);
|
this.logger.info(`Adding ${name} authentication strategy`);
|
||||||
this.passport.use(name, strategy);
|
this.passport.use(name, strategy);
|
||||||
|
this.OAuthParams[name] = authParams || null;
|
||||||
// Quick access getter to get the middleware for authenticating
|
// Quick access getter to get the middleware for authenticating
|
||||||
Object.defineProperty(this, name, {
|
Object.defineProperty(this, name, {
|
||||||
get: () => {
|
get: () => {
|
||||||
@ -171,6 +183,29 @@ class Authenticator {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOAuthLink (service, { ...rest } = {}) {
|
||||||
|
service = service.toLowerCase();
|
||||||
|
const link = OAuthLinks[service];
|
||||||
|
const OAuthParams = this.OAuthParams[service];
|
||||||
|
const params = {
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: OAuthParams.clientID,
|
||||||
|
redirect_uri: OAuthParams.callbackURL,
|
||||||
|
scope: OAuthParams.scope.join(' '),
|
||||||
|
state: null,
|
||||||
|
...rest
|
||||||
|
};
|
||||||
|
return `${link}?${Object.entries(params).filter(([ , v ]) => Boolean(v)).map(([ k, v ]) => `${k}=${v}`).join('&')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOAuthTokenParams (service) {
|
||||||
|
service = service.toLowerCase();
|
||||||
|
return {
|
||||||
|
link: OAuthLinks[service + 'Token'],
|
||||||
|
...this.OAuthParams[service]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Authenticator;
|
module.exports = Authenticator;
|
@ -4,7 +4,8 @@ const { Util } = require('../../util');
|
|||||||
const UserApplicataion = require('./UserApplication');
|
const UserApplicataion = require('./UserApplication');
|
||||||
|
|
||||||
// Fields omitted in safeJson
|
// Fields omitted in safeJson
|
||||||
const ProtectedFields = [ '_id', '_otpSecret', '_passwordHash' ];
|
// Should be keys used in the json, not the ones defined in the constructor
|
||||||
|
const ProtectedFields = [ '_id', 'otpSecret', 'password' ];
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ class User {
|
|||||||
this.cachedTimestamp = Date.now();
|
this.cachedTimestamp = Date.now();
|
||||||
this.createdTimestamp = data.createdTimestamp || Date.now();
|
this.createdTimestamp = data.createdTimestamp || Date.now();
|
||||||
|
|
||||||
this.avatarURL = data.avatarURL || null;
|
this.avatar = data.avatar || null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +181,7 @@ class User {
|
|||||||
applications: this._applications,
|
applications: this._applications,
|
||||||
createdTimestamp: this.createdTimestamp,
|
createdTimestamp: this.createdTimestamp,
|
||||||
disabled: this.disabled,
|
disabled: this.disabled,
|
||||||
avatarURL: this.avatarURL,
|
avatar: this.avatar,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,67 @@ class Util {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async #parseResponse (response) {
|
||||||
|
const { headers: rawHeaders, status } = response;
|
||||||
|
// Fetch returns heders as an interable for some reason
|
||||||
|
const headers = [ ...rawHeaders ].reduce((acc, [ key, val ]) => {
|
||||||
|
acc[key.toLowerCase()] = val;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
const success = status >= 200 && status < 300;
|
||||||
|
const base = { success, status };
|
||||||
|
|
||||||
|
if (headers['content-type']?.includes('application/json')) {
|
||||||
|
const data = await response.json();
|
||||||
|
return { ...base, data };
|
||||||
|
}
|
||||||
|
return { ...base, message: await response.text() };
|
||||||
|
}
|
||||||
|
|
||||||
|
static #params (params) {
|
||||||
|
return `${Object.entries(params)
|
||||||
|
.filter(([ , v ]) => Boolean(v))
|
||||||
|
.map(([ k, v ]) => `${encodeURI(k)}=${encodeURI(v)}`)
|
||||||
|
.join('&')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async get (url, options = {}) {
|
||||||
|
|
||||||
|
const { params, ...rest } = options;
|
||||||
|
if (params) url = Util.#params(url, params);
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...rest });
|
||||||
|
return Util.#parseResponse(response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static async post (url, options = {}) {
|
||||||
|
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
let { params, body, ...rest } = options;
|
||||||
|
if (params) url += '?' + Util.#params(params);
|
||||||
|
|
||||||
|
if (rest.headers) {
|
||||||
|
const headers = Object.keys(rest.headers);
|
||||||
|
const contentType = headers.find((k) => k.toLowerCase() === 'content-type');
|
||||||
|
|
||||||
|
if (contentType) {
|
||||||
|
const value = rest.headers[contentType].toLowerCase();
|
||||||
|
|
||||||
|
if (value === 'application/json' && typeof body !== 'string') {
|
||||||
|
body = JSON.stringify(body);
|
||||||
|
} else if (value === 'application/x-www-form-urlencoded') {
|
||||||
|
if (body) body = Util.#params(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, { ...rest, body, method: 'post' });
|
||||||
|
return Util.#parseResponse(response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
static checkPermissions (perms, perm, level = 1) {
|
static checkPermissions (perms, perm, level = 1) {
|
||||||
|
|
||||||
if (!perms || typeof perms !== 'object') throw new Error('Missing perms object');
|
if (!perms || typeof perms !== 'object') throw new Error('Missing perms object');
|
||||||
|
Loading…
Reference in New Issue
Block a user