mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-25 03:44:09 +01:00
✨ feat: Add custom fields & role assignment to OpenID strategy (#5612)
* started with Support for Customizable OpenID Profile Fields via Environment Variable * kept as much of the original code as possible but still added the custom data mapper * kept as much of the original code as possible but still added the custom data mapper * resolved merge conflicts * resolved merge conflicts * resolved merge conflicts * resolved merge conflicts * removed some unneeded comments * fix: conflicted issue --------- Co-authored-by: Talstra Ruben SRSNL <ruben.talstra@stadlerrail.com>
This commit is contained in:
parent
404b27d045
commit
2ef6e4462d
5 changed files with 250 additions and 2 deletions
168
api/strategies/OpenId/openidDataMapper.js
Normal file
168
api/strategies/OpenId/openidDataMapper.js
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
const fetch = require('node-fetch');
|
||||
const { HttpsProxyAgent } = require('https-proxy-agent');
|
||||
const { logger } = require('~/config');
|
||||
const { URL } = require('url');
|
||||
|
||||
// Microsoft SDK
|
||||
const { Client: MicrosoftGraphClient } = require('@microsoft/microsoft-graph-client');
|
||||
|
||||
/**
|
||||
* Base class for provider-specific data mappers.
|
||||
*/
|
||||
class BaseDataMapper {
|
||||
/**
|
||||
* Map custom OpenID data.
|
||||
* @param {string} accessToken - The access token to authenticate the request.
|
||||
* @param {string|Array<string>} customQuery - Either a full query string (if it contains operators)
|
||||
* or an array of fields to select.
|
||||
* @returns {Promise<Map<string, any>>} A promise that resolves to a map of custom fields.
|
||||
* @throws {Error} Throws an error if not implemented in the subclass.
|
||||
*/
|
||||
async mapCustomData(accessToken, customQuery) {
|
||||
throw new Error('mapCustomData() must be implemented by subclasses');
|
||||
}
|
||||
|
||||
/**
|
||||
* Optionally handle proxy settings for HTTP requests.
|
||||
* @returns {Object} Configuration object with proxy settings if PROXY is set.
|
||||
*/
|
||||
getProxyOptions() {
|
||||
if (process.env.PROXY) {
|
||||
const agent = new HttpsProxyAgent(process.env.PROXY);
|
||||
return { agent };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Microsoft-specific data mapper using the Microsoft Graph SDK.
|
||||
*/
|
||||
class MicrosoftDataMapper extends BaseDataMapper {
|
||||
/**
|
||||
* Initializes the MicrosoftGraphClient once for reuse.
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
this.accessToken = null;
|
||||
|
||||
this.client = MicrosoftGraphClient.init({
|
||||
defaultVersion: 'beta',
|
||||
authProvider: (done) => {
|
||||
// The authProvider will be called for each request to get the token
|
||||
if (this.accessToken) {
|
||||
done(null, this.accessToken);
|
||||
} else {
|
||||
done(new Error('Access token is not set.'), null);
|
||||
}
|
||||
},
|
||||
fetch: fetch,
|
||||
...this.getProxyOptions(),
|
||||
});
|
||||
|
||||
// Bind methods to maintain context
|
||||
this.mapCustomData = this.mapCustomData.bind(this);
|
||||
this.cleanData = this.cleanData.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the access token for the client.
|
||||
* This method should be called before making any requests.
|
||||
*
|
||||
* @param {string} accessToken - The access token.
|
||||
*/
|
||||
setAccessToken(accessToken) {
|
||||
if (!accessToken || typeof accessToken !== 'string') {
|
||||
throw new Error('[MicrosoftDataMapper] Invalid access token provided.');
|
||||
}
|
||||
this.accessToken = accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map custom OpenID data using the Microsoft Graph SDK.
|
||||
*
|
||||
* @param {string} accessToken - The access token to authenticate the request.
|
||||
* @param {string|Array<string>} customQuery - Fields to select from the Microsoft Graph API.
|
||||
* @returns {Promise<Map<string, any>>} A promise that resolves to a map of custom fields.
|
||||
*/
|
||||
async mapCustomData(accessToken, customQuery) {
|
||||
try {
|
||||
this.setAccessToken(accessToken);
|
||||
|
||||
if (!customQuery) {
|
||||
logger.warn('[MicrosoftDataMapper] No customQuery provided.');
|
||||
return new Map();
|
||||
}
|
||||
|
||||
// Convert customQuery to a comma-separated string if it's an array
|
||||
const fields = Array.isArray(customQuery) ? customQuery.join(',') : customQuery;
|
||||
|
||||
if (!fields) {
|
||||
logger.warn('[MicrosoftDataMapper] No fields specified in customQuery.');
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const result = await this.client
|
||||
.api('/me')
|
||||
.select(fields)
|
||||
.get();
|
||||
|
||||
const cleanedData = this.cleanData(result);
|
||||
return new Map(Object.entries(cleanedData));
|
||||
} catch (error) {
|
||||
// Handle specific Microsoft Graph errors if needed
|
||||
logger.error(`[MicrosoftDataMapper] Error fetching user data: ${error.message}`, { stack: error.stack });
|
||||
return new Map();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove all keys starting with @odata. from an object and convert Maps.
|
||||
*
|
||||
* @param {object|Array} obj - The object or array to clean.
|
||||
* @returns {object|Array} - The cleaned object or array.
|
||||
*/
|
||||
cleanData(obj) {
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(this.cleanData);
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
return Object.entries(obj).reduce((acc, [key, value]) => {
|
||||
if (!key.startsWith('@odata.')) {
|
||||
acc[key] = this.cleanData(value);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map provider names to their specific data mappers.
|
||||
*/
|
||||
const PROVIDER_MAPPERS = {
|
||||
// Fully Working
|
||||
microsoft: MicrosoftDataMapper,
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstraction layer that returns a provider-specific mapper instance.
|
||||
*/
|
||||
class OpenIdDataMapper {
|
||||
/**
|
||||
* Retrieve an instance of the mapper for the specified provider.
|
||||
*
|
||||
* @param {string} provider - The name of the provider (e.g., 'microsoft').
|
||||
* @returns {BaseDataMapper} An instance of the specific data mapper for the provider.
|
||||
* @throws {Error} Throws an error if no mapper is found for the specified provider.
|
||||
*/
|
||||
static getMapper(provider) {
|
||||
const MapperClass = PROVIDER_MAPPERS[provider.toLowerCase()];
|
||||
if (!MapperClass) {
|
||||
throw new Error(`No mapper found for provider: ${provider}`);
|
||||
}
|
||||
return new MapperClass();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = OpenIdDataMapper;
|
||||
Loading…
Add table
Add a link
Reference in a new issue