UNPKG

digitaltwin-core

Version:

Minimalist framework to collect and handle data in a Digital Twin project

237 lines 9.39 kB
import { AuthConfig } from './auth_config.js'; /** * Service for managing users in the Digital Twin framework. * * This service handles the complete user lifecycle in a Digital Twin application * with Keycloak authentication via Apache APISIX. It manages a normalized database * schema with three tables: * * - `users`: Core user records linked to Keycloak IDs * - `roles`: Master list of available roles * - `user_roles`: Many-to-many relationship between users and roles * * Key features: * - Automatic user creation on first authentication * - Role synchronization with Keycloak * - Optimized queries with proper indexing * - Transaction-safe role updates * * @example * ```typescript * // Initialize in your Digital Twin engine * const userService = new UserService(databaseAdapter) * await userService.initializeTables() * * // Use in AssetsManager handlers * const authUser = ApisixAuthParser.parseAuthHeaders(req.headers) * const userRecord = await userService.findOrCreateUser(authUser!) * * // Link assets to users * await this.uploadAsset({ * description: 'My file', * source: 'upload', * owner_id: userRecord.id!.toString(), * filename: 'document.pdf', * file: buffer * }) * ``` */ export class UserService { constructor(db) { this.usersTable = 'users'; this.rolesTable = 'roles'; this.userRolesTable = 'user_roles'; this.db = db; } /** * Ensures all user-related tables exist in the database */ async initializeTables() { const knex = this.getKnex(); // 1. Create roles table if (!(await knex.schema.hasTable(this.rolesTable))) { await knex.schema.createTable(this.rolesTable, table => { table.increments('id').primary(); table.string('name', 100).notNullable().unique(); table.timestamp('created_at').defaultTo(knex.fn.now()); // Index pour les recherches par nom de rôle table.index('name', 'roles_idx_name'); }); } // 2. Create users table if (!(await knex.schema.hasTable(this.usersTable))) { await knex.schema.createTable(this.usersTable, table => { table.increments('id').primary(); table.string('keycloak_id', 255).notNullable().unique(); table.timestamp('created_at').defaultTo(knex.fn.now()); table.timestamp('updated_at').defaultTo(knex.fn.now()); // Index principal pour les lookups par keycloak_id table.index('keycloak_id', 'users_idx_keycloak_id'); table.index('created_at', 'users_idx_created_at'); }); } // 3. Create user_roles junction table if (!(await knex.schema.hasTable(this.userRolesTable))) { await knex.schema.createTable(this.userRolesTable, table => { table.integer('user_id').unsigned().notNullable(); table.integer('role_id').unsigned().notNullable(); table.timestamp('created_at').defaultTo(knex.fn.now()); // Clé primaire composite table.primary(['user_id', 'role_id']); // Clés étrangères table.foreign('user_id').references('id').inTable(this.usersTable).onDelete('CASCADE'); table.foreign('role_id').references('id').inTable(this.rolesTable).onDelete('CASCADE'); // Index pour les requêtes inverses (quels utilisateurs ont ce rôle) table.index('role_id', 'user_roles_idx_role_id'); table.index('user_id', 'user_roles_idx_user_id'); }); } } /** * Finds or creates a user and synchronizes their roles. * * When authentication is disabled, returns a mock user record * without touching the database. */ async findOrCreateUser(authUser) { // If authentication is disabled, return a mock user record if (AuthConfig.isAuthDisabled()) { return { id: 1, // Use a consistent ID for anonymous user keycloak_id: authUser.id, roles: authUser.roles, created_at: new Date(), updated_at: new Date() }; } // 1. Find or create user let userRecord = await this.findUserByKeycloakId(authUser.id); if (!userRecord) { userRecord = await this.createUser(authUser); } if (!userRecord.id) { throw new Error('User record does not have an ID after creation/retrieval'); } // 2. Synchronize roles await this.syncUserRoles(userRecord.id, authUser.roles); // 3. Return user with current roles return (await this.getUserWithRoles(userRecord.id)) || userRecord; } /** * Gets a user by their database ID */ async getUserById(id) { return await this.getUserWithRoles(id); } /** * Gets a user by their Keycloak ID with roles */ async getUserByKeycloakId(keycloakId) { const knex = this.getKnex(); const userRow = (await knex(this.usersTable).where('keycloak_id', keycloakId).first()); if (!userRow) return undefined; return await this.getUserWithRoles(userRow.id); } /** * Gets the underlying Knex instance from the database adapter */ getKnex() { if ('getKnex' in this.db && typeof this.db.getKnex === 'function') { return this.db.getKnex(); } throw new Error('Cannot access Knex instance from DatabaseAdapter'); } /** * Finds a user by their Keycloak ID */ async findUserByKeycloakId(keycloakId) { const knex = this.getKnex(); const row = await knex(this.usersTable).where('keycloak_id', keycloakId).first(); if (!row) return undefined; return { id: row.id, keycloak_id: row.keycloak_id, roles: [], // Will be populated by getUserWithRoles created_at: new Date(row.created_at), updated_at: new Date(row.updated_at) }; } /** * Creates a new user record */ async createUser(authUser) { const knex = this.getKnex(); const now = new Date(); const userData = { keycloak_id: authUser.id, created_at: now, updated_at: now }; const insertResult = await knex(this.usersTable).insert(userData).returning('id'); const insertedId = insertResult[0]; const id = typeof insertedId === 'object' ? insertedId.id : insertedId; return { id, keycloak_id: authUser.id, roles: [], created_at: now, updated_at: now }; } /** * Synchronizes user roles with what's provided by Keycloak */ async syncUserRoles(userId, newRoles) { const knex = this.getKnex(); // Transaction pour assurer la cohérence await knex.transaction(async (trx) => { // 1. Ensure all roles exist in roles table for (const roleName of newRoles) { await trx(this.rolesTable).insert({ name: roleName }).onConflict('name').ignore(); // Si le rôle existe déjà, on l'ignore } // 2. Get role IDs const roleRows = (await trx(this.rolesTable).select('id', 'name').whereIn('name', newRoles)); const roleIds = roleRows.map(r => r.id); // 3. Remove old role associations await trx(this.userRolesTable).where('user_id', userId).delete(); // 4. Add new role associations if (roleIds.length > 0) { const userRoleData = roleIds.map((roleId) => ({ user_id: userId, role_id: roleId })); await trx(this.userRolesTable).insert(userRoleData); } // 5. Update user's updated_at timestamp await trx(this.usersTable).where('id', userId).update({ updated_at: new Date() }); }); } /** * Gets a user with their roles populated */ async getUserWithRoles(userId) { const knex = this.getKnex(); // Join query to get user + roles const result = (await knex(this.usersTable) .leftJoin(this.userRolesTable, `${this.usersTable}.id`, `${this.userRolesTable}.user_id`) .leftJoin(this.rolesTable, `${this.userRolesTable}.role_id`, `${this.rolesTable}.id`) .select(`${this.usersTable}.id`, `${this.usersTable}.keycloak_id`, `${this.usersTable}.created_at`, `${this.usersTable}.updated_at`, `${this.rolesTable}.name as role_name`) .where(`${this.usersTable}.id`, userId)); if (result.length === 0) return undefined; const userRow = result[0]; const roles = result .filter((row) => row.role_name !== null) .map(row => row.role_name); return { id: userRow.id, keycloak_id: userRow.keycloak_id, roles: roles, created_at: new Date(userRow.created_at), updated_at: new Date(userRow.updated_at) }; } } //# sourceMappingURL=user_service.js.map