Files
web-antrean/server/api/users/sync-all.post.ts
2026-01-07 07:50:25 +07:00

393 lines
12 KiB
TypeScript

// server/api/users/sync-all.post.ts
// Sync all users from Keycloak Admin API to database
// This endpoint will fetch all users from Keycloak and sync them to the database
import Database from 'better-sqlite3';
import { join } from 'path';
import { existsSync, mkdirSync } from 'fs';
// Helper to get database path
const getDbPath = () => {
const dbDir = join(process.cwd(), 'data');
if (!existsSync(dbDir)) {
mkdirSync(dbDir, { recursive: true });
}
return join(dbDir, 'users.db');
};
// Helper to decode JWT token payload
const decodeTokenPayload = (token: string | undefined): any | null => {
if (!token) return null;
try {
const parts = token.split(".");
if (parts.length < 2) return null;
const payloadBase64 = parts[1];
return JSON.parse(Buffer.from(payloadBase64, "base64").toString());
} catch (e) {
return null;
}
};
// Helper to get last access from Keycloak for a user
const getLastAccessFromKeycloak = async (userId: string, accessToken: string, config: any): Promise<number | null> => {
try {
if (!accessToken) {
return null;
}
const issuerUrl = new URL(config.keycloakIssuer);
const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master';
const sessionsUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}/users/${userId}/sessions`;
const sessionsResponse = await fetch(sessionsUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
if (!sessionsResponse.ok) {
return null;
}
const sessions = await sessionsResponse.json() as any[];
if (!sessions || sessions.length === 0) {
return null;
}
let lastAccessTimestamp = 0;
sessions.forEach(session => {
if (session.lastAccess && session.lastAccess > lastAccessTimestamp) {
lastAccessTimestamp = session.lastAccess;
}
});
return lastAccessTimestamp > 0 ? Math.floor(lastAccessTimestamp / 1000) : null;
} catch (error: any) {
console.warn(`⚠️ Error fetching last access for user ${userId}:`, error.message);
return null;
}
};
// Helper to get user from Keycloak Admin API
const getUserFromKeycloak = async (userId: string, accessToken: string, config: any): Promise<any | null> => {
try {
if (!accessToken) {
return null;
}
const issuerUrl = new URL(config.keycloakIssuer);
const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master';
const userUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}/users/${userId}`;
const userResponse = await fetch(userUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
if (!userResponse.ok) {
return null;
}
return await userResponse.json();
} catch (error: any) {
console.warn(`⚠️ Error fetching user ${userId} from Keycloak:`, error.message);
return null;
}
};
// Helper to get all users from Keycloak Admin API
const getAllUsersFromKeycloak = async (accessToken: string, config: any): Promise<any[]> => {
try {
if (!accessToken) {
return [];
}
const issuerUrl = new URL(config.keycloakIssuer);
const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master';
const usersUrl = `${config.keycloakIssuer.replace('/realms/' + realm, '')}/admin/realms/${realm}/users?max=1000`;
const usersResponse = await fetch(usersUrl, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
if (!usersResponse.ok) {
console.warn('⚠️ Failed to fetch users from Keycloak:', usersResponse.status);
return [];
}
return await usersResponse.json();
} catch (error: any) {
console.warn(`⚠️ Error fetching all users from Keycloak:`, error.message);
return [];
}
};
// Initialize database
const initDb = () => {
const dbPath = getDbPath();
const db = new Database(dbPath);
// Create table if not exists
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
namaLengkap TEXT NOT NULL,
namaUser TEXT UNIQUE NOT NULL,
email TEXT,
tipeUser TEXT DEFAULT '',
lastLogin INTEGER,
roles TEXT DEFAULT '[]',
realmRoles TEXT DEFAULT '[]',
accountRoles TEXT DEFAULT '[]',
resourceRoles TEXT DEFAULT '[]',
groups TEXT DEFAULT '[]',
given_name TEXT,
family_name TEXT,
createdAt INTEGER DEFAULT (strftime('%s', 'now')),
updatedAt INTEGER DEFAULT (strftime('%s', 'now'))
)
`);
// Migration: Check and add missing columns one by one
try {
const tableInfo = db.prepare("PRAGMA table_info(users)").all() as any[];
const columnNames = tableInfo.map(col => col.name);
// Add missing columns one by one
if (!columnNames.includes('realmRoles')) {
db.exec(`ALTER TABLE users ADD COLUMN realmRoles TEXT DEFAULT '[]'`);
console.log('✅ Added column: realmRoles');
}
if (!columnNames.includes('accountRoles')) {
db.exec(`ALTER TABLE users ADD COLUMN accountRoles TEXT DEFAULT '[]'`);
console.log('✅ Added column: accountRoles');
}
if (!columnNames.includes('resourceRoles')) {
db.exec(`ALTER TABLE users ADD COLUMN resourceRoles TEXT DEFAULT '[]'`);
console.log('✅ Added column: resourceRoles');
}
if (!columnNames.includes('lastLogin')) {
db.exec(`ALTER TABLE users ADD COLUMN lastLogin INTEGER`);
console.log('✅ Added column: lastLogin');
}
if (!columnNames.includes('given_name')) {
db.exec(`ALTER TABLE users ADD COLUMN given_name TEXT`);
console.log('✅ Added column: given_name');
}
if (!columnNames.includes('family_name')) {
db.exec(`ALTER TABLE users ADD COLUMN family_name TEXT`);
console.log('✅ Added column: family_name');
}
} catch (e: any) {
console.error('❌ Migration error:', e.message);
// Don't throw, continue with existing schema
}
return db;
};
export default defineEventHandler(async (event) => {
console.log("🔄 Sync all users endpoint called");
// Get session from session store
const { getSessionFromCookie } = await import('~/server/utils/sessionStore');
const session = await getSessionFromCookie(event);
if (!session) {
throw createError({
statusCode: 401,
statusMessage: "No session found or session expired",
});
}
try {
const config = useRuntimeConfig();
const accessToken = session.accessToken;
if (!accessToken) {
throw createError({
statusCode: 401,
statusMessage: "No access token found",
});
}
// Get all users from Keycloak
console.log("📥 Fetching all users from Keycloak...");
const keycloakUsers = await getAllUsersFromKeycloak(accessToken, config);
console.log(`✅ Found ${keycloakUsers.length} users in Keycloak`);
const db = initDb();
let createdCount = 0;
let updatedCount = 0;
let unchangedCount = 0;
// Sync each user
for (const kcUser of keycloakUsers) {
try {
const userId = kcUser.id;
const namaLengkap = kcUser.firstName && kcUser.lastName
? `${kcUser.firstName} ${kcUser.lastName}`.trim()
: kcUser.firstName || kcUser.lastName || kcUser.username || '';
const namaUser = kcUser.username || kcUser.email?.split('@')[0] || '';
const email = kcUser.email || null;
const given_name = kcUser.firstName || null;
const family_name = kcUser.lastName || null;
if (!userId || !namaUser) {
console.warn(`⚠️ Skipping user with missing ID or username: ${userId}`);
continue;
}
// Get user details from Keycloak (including roles and groups)
const userDetails = await getUserFromKeycloak(userId, accessToken, config);
// Extract roles and groups
const realmRoles = userDetails?.realmRoles || [];
const groups = userDetails?.groups || [];
// Determine tipeUser from groups
let tipeUser = '';
if (Array.isArray(groups) && groups.length > 0) {
const lastGroup = groups[groups.length - 1];
if (typeof lastGroup === 'string') {
const parts = lastGroup.split('/').filter(Boolean);
if (parts.length > 0) {
tipeUser = parts[parts.length - 1];
}
}
}
// Get last access from Keycloak
const lastLogin = await getLastAccessFromKeycloak(userId, accessToken, config);
// Check if user exists in database
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(userId) as any;
const rolesJson = JSON.stringify(realmRoles);
const realmRolesJson = JSON.stringify(realmRoles);
const accountRolesJson = JSON.stringify([]);
const resourceRolesJson = JSON.stringify([]);
const groupsJson = JSON.stringify(groups);
if (existingUser) {
// Update existing user - only update if there are changes
const needsUpdate =
existingUser.namaLengkap !== namaLengkap ||
existingUser.namaUser !== namaUser ||
existingUser.email !== email ||
existingUser.roles !== rolesJson ||
existingUser.realmRoles !== realmRolesJson ||
existingUser.groups !== groupsJson ||
existingUser.given_name !== given_name ||
existingUser.family_name !== family_name ||
(existingUser.tipeUser === '' && tipeUser !== '') ||
(lastLogin && existingUser.lastLogin !== lastLogin);
if (needsUpdate) {
const updateTipeUser = existingUser.tipeUser === '' ? tipeUser : existingUser.tipeUser;
const updateLastLogin = lastLogin || existingUser.lastLogin;
db.prepare(`
UPDATE users
SET namaLengkap = ?,
namaUser = ?,
email = ?,
roles = ?,
realmRoles = ?,
accountRoles = ?,
resourceRoles = ?,
groups = ?,
given_name = ?,
family_name = ?,
tipeUser = ?,
lastLogin = ?,
updatedAt = strftime('%s', 'now')
WHERE id = ?
`).run(
namaLengkap,
namaUser,
email || null,
rolesJson,
realmRolesJson,
accountRolesJson,
resourceRolesJson,
groupsJson,
given_name,
family_name,
updateTipeUser,
updateLastLogin,
userId
);
updatedCount++;
console.log(`✅ Updated user: ${namaUser}`);
} else {
unchangedCount++;
}
} else {
// Insert new user
db.prepare(`
INSERT INTO users (
id, namaLengkap, namaUser, email, roles, realmRoles, accountRoles, resourceRoles, groups,
given_name, family_name, tipeUser, lastLogin
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId,
namaLengkap,
namaUser,
email || null,
rolesJson,
realmRolesJson,
accountRolesJson,
resourceRolesJson,
groupsJson,
given_name,
family_name,
tipeUser,
lastLogin
);
createdCount++;
console.log(`✅ Created new user: ${namaUser}`);
}
} catch (userError: any) {
console.error(`❌ Error syncing user ${kcUser.id}:`, userError.message);
// Continue with next user
}
}
db.close();
console.log(`✅ Sync completed: ${createdCount} created, ${updatedCount} updated, ${unchangedCount} unchanged`);
return {
success: true,
message: 'All users synced successfully',
stats: {
created: createdCount,
updated: updatedCount,
unchanged: unchangedCount,
total: keycloakUsers.length
}
};
} catch (error: any) {
console.error("❌ Error syncing all users:", error);
throw createError({
statusCode: 500,
statusMessage: error.message || "Failed to sync all users",
});
}
});