@doneisbetter/sso
Version:
A secure, privacy-focused SSO solution with ephemeral token handling
526 lines • 22 kB
JavaScript
import { MongoClient, ObjectId } from 'mongodb';
import { generateIdentityProfile } from '../utils/identity';
import crypto from 'crypto';
// Configure MongoDB connection based on environment
const getMongoConfig = () => {
const uri = process.env.MONGODB_URI;
if (!uri) {
if (process.env.NODE_ENV === 'test') {
// For tests, use a default test configuration
return {
uri: 'mongodb+srv://moldovancsaba:togwa1-xyhcEp-mozceb@mongodb-thanperfect.zf2o0ix.mongodb.net/?retryWrites=true&w=majority&appName=mongodb-thanperfect',
dbName: 'sso_test'
};
}
throw new Error('MongoDB URI is not configured');
}
return { uri, dbName: process.env.NODE_ENV === 'test' ? 'sso_test' : 'sso' };
};
const config = getMongoConfig();
const options = {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
};
let client;
let clientPromise;
// Handle connection based on environment
if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') {
// Use global variable in development/test to prevent multiple connections
let globalWithMongo = global;
if (!globalWithMongo._mongoClientPromise) {
client = new MongoClient(config.uri, options);
globalWithMongo._mongoClientPromise = client.connect();
}
clientPromise = globalWithMongo._mongoClientPromise;
}
else {
// In production, create a new connection
client = new MongoClient(config.uri, options);
clientPromise = client.connect();
}
export class Database {
constructor() {
this.client = null;
this.db = null;
}
static async getInstance() {
if (!Database.instance) {
Database.instance = new Database();
Database.instance.client = await clientPromise;
const dbName = process.env.NODE_ENV === 'test' ? 'sso_test' : 'sso';
Database.instance.db = Database.instance.client.db(dbName);
}
return Database.instance;
}
// Tenant operations
async validateApiKey(apiKey) {
if (!this.db)
throw new Error('Database not initialized');
const tenant = await this.db.collection('tenants').findOne({
'apiKeys.key': apiKey
});
if (tenant) {
// Update last used timestamp
const now = new Date().toISOString();
await this.db.collection('tenants').updateOne({ 'apiKeys.key': apiKey }, {
$set: {
'apiKeys.$.lastUsed': now,
updatedAt: now
}
});
// Update the tenant object with new timestamps
const apiKeyEntry = tenant.apiKeys.find(k => k.key === apiKey);
if (apiKeyEntry) {
apiKeyEntry.lastUsed = now;
}
tenant.updatedAt = now;
}
return tenant;
}
// OAuth operations
async createOAuthClient(name, redirectUris) {
if (!this.db)
throw new Error('Database not initialized');
const clientId = crypto.randomBytes(16).toString('hex');
const clientSecret = crypto.randomBytes(32).toString('hex');
const now = new Date().toISOString();
const client = {
id: new ObjectId().toString(),
name,
clientId,
clientSecret,
redirectUris,
createdAt: now,
updatedAt: now
};
await this.db.collection('oauth_clients').insertOne(client);
return client;
}
async validateOAuthClient(clientId, clientSecret) {
if (!this.db)
throw new Error('Database not initialized');
const query = { clientId };
if (clientSecret) {
query.clientSecret = clientSecret;
}
const client = await this.db.collection('oauth_clients').findOne(query);
return client ? Object.assign(Object.assign({}, client), { id: client._id.toString() }) : null;
}
async listOAuthClients() {
if (!this.db)
throw new Error('Database not initialized');
const clients = await this.db.collection('oauth_clients').find({}).toArray();
return clients.map(client => (Object.assign(Object.assign({}, client), { id: client._id.toString() })));
}
// Update user data
async updateUser(id, update) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('users').findOneAndUpdate({ _id: new ObjectId(id) }, { $set: update }, { returnDocument: 'after' });
return result.value ? {
id: result.value._id.toString(),
identifier: result.value.identifier,
email: result.value.email,
emailVerified: result.value.emailVerified,
identityId: result.value.identityId,
profile: result.value.profile,
metadata: result.value.metadata,
createdAt: result.value.createdAt,
lastLoginAt: result.value.lastLoginAt,
updatedAt: result.value.updatedAt || result.value.createdAt
} : null;
}
// OAuth operations
async updateOAuthClient(id, update) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('oauth_clients').findOneAndUpdate({ _id: new ObjectId(id) }, {
$set: Object.assign(Object.assign({}, update), { updatedAt: new Date().toISOString() })
}, { returnDocument: 'after' });
return result.value ? Object.assign(Object.assign({}, result.value), { id: result.value._id.toString() }) : null;
}
// User operations
async findUser(identifierOrEmail) {
if (!this.db)
throw new Error('Database not initialized');
const user = await this.db.collection('users').findOne({
$or: [
{ identifier: identifierOrEmail },
{ email: identifierOrEmail }
]
});
if (!user)
return null;
return {
id: user._id.toString(),
identifier: user.identifier,
email: user.email,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
updatedAt: user.updatedAt || user.createdAt
};
}
async createOrUpdateUser(identifier, options) {
var _a;
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const collection = this.db.collection('users');
const query = (options === null || options === void 0 ? void 0 : options.email) ? { $or: [{ identifier }, { email: options.email }] } : { identifier };
const existingUser = await collection.findOne(query);
if (existingUser) {
// Update user data and last login
const updateData = {
lastLoginAt: now,
updatedAt: now
};
if (options) {
if (options.email)
updateData.email = options.email;
if (options.emailVerified !== undefined)
updateData.emailVerified = options.emailVerified;
if (options.profile)
updateData.profile = options.profile;
if (options.metadata)
updateData.metadata = options.metadata;
}
await collection.updateOne({ _id: existingUser._id }, { $set: updateData });
return {
id: existingUser._id.toString(),
identifier: existingUser.identifier,
email: existingUser.email,
emailVerified: existingUser.emailVerified,
identityId: existingUser.identityId,
profile: existingUser.profile,
metadata: existingUser.metadata,
createdAt: existingUser.createdAt,
lastLoginAt: now,
updatedAt: now
};
}
// Create new identity for new user with provided emoji/color or generate default
const identityProfile = (options === null || options === void 0 ? void 0 : options.emoji) && (options === null || options === void 0 ? void 0 : options.color)
? {
gametag: identifier,
emoji: options.emoji,
color: options.color,
createdAt: now,
updatedAt: now
}
: await generateIdentityProfile(identifier);
const identity = await this.createIdentity(identityProfile);
// Create new user with identity reference and enhanced profile
const userData = {
identifier,
email: options === null || options === void 0 ? void 0 : options.email,
emailVerified: (_a = options === null || options === void 0 ? void 0 : options.emailVerified) !== null && _a !== void 0 ? _a : false,
identityId: identity._id,
profile: (options === null || options === void 0 ? void 0 : options.profile) || {},
metadata: (options === null || options === void 0 ? void 0 : options.metadata) || {},
createdAt: now,
lastLoginAt: now,
updatedAt: now
};
const result = await collection.insertOne(userData);
return {
id: result.insertedId.toString(),
identifier,
email: options === null || options === void 0 ? void 0 : options.email,
emailVerified: userData.emailVerified,
identityId: identity._id,
profile: userData.profile,
metadata: userData.metadata,
createdAt: now,
lastLoginAt: now,
updatedAt: now
};
}
// URL Configuration Management
async createURLConfig(input) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const urlConfig = Object.assign(Object.assign({ _id: new ObjectId() }, input), { isDefault: input.isDefault || false, createdAt: now, updatedAt: now });
// If this config is set as default, unset any existing default for the same environment
if (urlConfig.isDefault) {
await this.db.collection('url_configs').updateMany({ environment: input.environment, isDefault: true }, { $set: { isDefault: false, updatedAt: now } });
}
await this.db.collection('url_configs').insertOne(urlConfig);
return urlConfig;
}
async updateURLConfig(id, input) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const update = Object.assign(Object.assign({}, input), { updatedAt: now });
// If setting as default, unset any existing default for the same environment
if (input.isDefault) {
const config = await this.db.collection('url_configs').findOne({ _id: new ObjectId(id) });
if (config) {
await this.db.collection('url_configs').updateMany({ environment: config.environment, isDefault: true }, { $set: { isDefault: false, updatedAt: now } });
}
}
const result = await this.db.collection('url_configs').findOneAndUpdate({ _id: new ObjectId(id) }, { $set: update }, { returnDocument: 'after' });
return result.value || null;
}
async deleteURLConfig(id) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('url_configs').deleteOne({ _id: new ObjectId(id) });
return result.deletedCount === 1;
}
async getURLConfig(id) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('url_configs').findOne({ _id: new ObjectId(id) });
}
async getDefaultURLConfig(environment) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('url_configs').findOne({
environment,
isDefault: true
});
}
async listURLConfigs(tenantId) {
if (!this.db)
throw new Error('Database not initialized');
const query = tenantId ? { tenantId } : {};
return this.db.collection('url_configs')
.find(query)
.sort({ isDefault: -1, createdAt: -1 })
.toArray();
}
async validateCallbackUrl(url, environment) {
if (!this.db)
throw new Error('Database not initialized');
const config = await this.db.collection('url_configs').findOne({
environment,
callbackUrls: url
});
return !!config;
}
// Identity Management
async createIdentity(identity) {
if (!this.db)
throw new Error('Database not initialized');
const doc = Object.assign(Object.assign({ _id: new ObjectId().toString() }, identity), { tenantId: 'default' });
await this.db.collection('identities').insertOne(doc);
return doc;
}
async getIdentity(id) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('identities').findOne({ _id: id });
}
async updateIdentity(id, update) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('identities').findOneAndUpdate({ _id: id }, { $set: Object.assign(Object.assign({}, update), { updatedAt: new Date().toISOString() }) }, { returnDocument: 'after' });
return result.value || null;
}
async deleteIdentity(id) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('identities').deleteOne({ _id: id });
return result.deletedCount === 1;
}
// Authentication Data Management
async linkAuthData(userId, authData) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
await this.db.collection('user_auth_data').updateOne({ userId, provider: authData.provider }, {
$set: Object.assign(Object.assign({}, authData), { updatedAt: now }),
$setOnInsert: {
createdAt: now
}
}, { upsert: true });
}
async unlinkAuthData(userId, provider) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('user_auth_data').deleteOne({ userId, provider });
return result.deletedCount === 1;
}
async getAuthDataForUser(userId) {
if (!this.db)
throw new Error('Database not initialized');
const authData = await this.db.collection('user_auth_data')
.find({ userId })
.sort({ createdAt: -1 })
.toArray();
return authData.map(doc => ({
userId: doc.userId,
provider: doc.provider,
providerId: doc.providerId,
accessToken: doc.accessToken,
refreshToken: doc.refreshToken,
createdAt: doc.createdAt,
updatedAt: doc.updatedAt
}));
}
async findUserByAuthData(provider, providerId) {
if (!this.db)
throw new Error('Database not initialized');
const authData = await this.db.collection('user_auth_data').findOne({ provider, providerId });
if (!authData)
return null;
const user = await this.db.collection('users').findOne({ _id: new ObjectId(authData.userId) });
if (!user)
return null;
return {
id: user._id.toString(),
identifier: user.identifier,
email: user.email,
identityId: user.identityId,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
updatedAt: user.updatedAt || user.createdAt
};
}
async updateAuthTokens(userId, provider, tokens) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const result = await this.db.collection('user_auth_data').updateOne({ userId, provider }, {
$set: Object.assign(Object.assign({}, tokens), { updatedAt: now })
});
return result.modifiedCount === 1;
}
// OAuth Token Management
async createOAuthToken(token) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const oauthToken = Object.assign(Object.assign({}, token), { createdAt: now });
await this.db.collection('oauth_tokens').insertOne(oauthToken);
return oauthToken;
}
async getOAuthToken(accessToken) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('oauth_tokens').findOne({ accessToken });
}
async getOAuthTokenByRefresh(refreshToken) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('oauth_tokens').findOne({ refreshToken });
}
async deleteOAuthToken(accessToken) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('oauth_tokens').deleteOne({ accessToken });
return result.deletedCount === 1;
}
// OAuth Code Management
async createOAuthCode(code) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const oauthCode = Object.assign(Object.assign({}, code), { createdAt: now });
await this.db.collection('oauth_codes').insertOne(oauthCode);
return oauthCode;
}
async getOAuthCode(code) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('oauth_codes').findOne({ code });
}
async markOAuthCodeAsUsed(code) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('oauth_codes').updateOne({ code }, { $set: { usedAt: new Date().toISOString() } });
return result.modifiedCount === 1;
}
// OAuth Consent Management
async createOrUpdateConsent(consent) {
if (!this.db)
throw new Error('Database not initialized');
const now = new Date().toISOString();
const oauthConsent = Object.assign(Object.assign({}, consent), { createdAt: now });
await this.db.collection('oauth_consents').updateOne({ userId: consent.userId, clientId: consent.clientId }, { $set: oauthConsent }, { upsert: true });
return oauthConsent;
}
async getConsent(userId, clientId) {
if (!this.db)
throw new Error('Database not initialized');
return this.db.collection('oauth_consents').findOne({ userId, clientId });
}
async revokeConsent(userId, clientId) {
if (!this.db)
throw new Error('Database not initialized');
const result = await this.db.collection('oauth_consents').deleteOne({ userId, clientId });
return result.deletedCount === 1;
}
// Audit Log Management
async createAuditLog(log) {
if (!this.db)
throw new Error('Database not initialized');
const auditLog = Object.assign(Object.assign({}, log), { timestamp: new Date().toISOString() });
await this.db.collection('audit_logs').insertOne(auditLog);
return auditLog;
}
async getAuditLogs(query) {
if (!this.db)
throw new Error('Database not initialized');
const filter = {};
if (query.eventType)
filter.eventType = query.eventType;
if (query.actorId)
filter.actorId = query.actorId;
if (query.targetId)
filter.targetId = query.targetId;
if (query.fromTimestamp || query.toTimestamp) {
filter.timestamp = {};
if (query.fromTimestamp)
filter.timestamp.$gte = query.fromTimestamp;
if (query.toTimestamp)
filter.timestamp.$lte = query.toTimestamp;
}
return this.db.collection('audit_logs')
.find(filter)
.sort({ timestamp: -1 })
.limit(query.limit || 100)
.toArray();
}
// User validation
async validateUserCredentials(username, password) {
if (!this.db)
throw new Error('Database not initialized');
// Find user by identifier or email
const user = await this.findUser(username);
if (!user)
return null;
// In a real implementation, you would validate the password here
// This is a placeholder implementation for development
return user;
}
// Enhanced user operations
async createOrUpdateUserWithAuth(identifier, authData, options) {
if (!this.db)
throw new Error('Database not initialized');
const user = await this.createOrUpdateUser(identifier, options);
await this.linkAuthData(user.id, authData);
return user;
}
async listUsers() {
if (!this.db)
throw new Error('Database not initialized');
const users = await this.db
.collection('users')
.find({})
.sort({ createdAt: -1 })
.toArray();
return users.map(user => ({
id: user._id.toString(),
identifier: user.identifier,
email: user.email,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
updatedAt: user.updatedAt || user.createdAt
}));
}
}
//# sourceMappingURL=database.js.map