215 lines
7.2 KiB
TypeScript
215 lines
7.2 KiB
TypeScript
// server/api/users/list.get.ts
|
||
// Get all users from database and enrich with last access from Keycloak
|
||
|
||
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 using access token from session
|
||
const getLastAccessFromKeycloak = async (userId: string, accessToken: string, config: any): Promise<number | null> => {
|
||
try {
|
||
if (!accessToken) {
|
||
return null;
|
||
}
|
||
|
||
// Extract realm from issuer
|
||
const issuerUrl = new URL(config.keycloakIssuer);
|
||
const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master';
|
||
|
||
// Get user sessions from Keycloak Admin API using access token
|
||
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) {
|
||
if (sessionsResponse.status === 404 || sessionsResponse.status === 403) {
|
||
// User has no sessions or no permission
|
||
return null;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
const sessions = await sessionsResponse.json() as any[];
|
||
|
||
if (!sessions || sessions.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
// Find the most recent session (highest lastAccess timestamp)
|
||
let lastAccessTimestamp = 0;
|
||
sessions.forEach(session => {
|
||
if (session.lastAccess && session.lastAccess > lastAccessTimestamp) {
|
||
lastAccessTimestamp = session.lastAccess;
|
||
}
|
||
});
|
||
|
||
// Convert from milliseconds to seconds (Unix timestamp)
|
||
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 ensure database schema is up to date
|
||
const ensureSchema = (db: any) => {
|
||
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('❌ Schema migration error:', e.message);
|
||
}
|
||
};
|
||
|
||
export default defineEventHandler(async (event) => {
|
||
console.log("📋 Users list endpoint called");
|
||
|
||
try {
|
||
const config = useRuntimeConfig();
|
||
const dbPath = getDbPath();
|
||
|
||
// Check if database exists
|
||
if (!existsSync(dbPath)) {
|
||
console.log("ℹ️ Database not found, returning empty array");
|
||
return [];
|
||
}
|
||
|
||
// Try to get access token from current user session
|
||
let accessToken: string | null = null;
|
||
try {
|
||
const { getSessionFromCookie } = await import('~/server/utils/sessionStore');
|
||
const session = await getSessionFromCookie(event);
|
||
if (session && session.accessToken) {
|
||
accessToken = session.accessToken;
|
||
}
|
||
} catch (e) {
|
||
// No session available, will skip Keycloak fetch
|
||
console.log("ℹ️ No valid session found, will use database values for last access");
|
||
}
|
||
|
||
// Open database connection
|
||
const db = new Database(dbPath);
|
||
|
||
// Ensure schema is up to date before querying
|
||
ensureSchema(db);
|
||
|
||
// Get all users
|
||
const users = db.prepare('SELECT * FROM users ORDER BY updatedAt DESC').all() as any[];
|
||
|
||
// Parse JSON fields and enrich with last access from Keycloak
|
||
const formattedUsers = await Promise.all(users.map(async (user) => {
|
||
// Get lastLogin from database (handle null, 0, or undefined)
|
||
let lastLogin: number | null = null;
|
||
if (user.lastLogin !== null && user.lastLogin !== undefined && user.lastLogin !== 0) {
|
||
lastLogin = user.lastLogin;
|
||
}
|
||
|
||
// Only fetch from Keycloak if we have a valid user ID, access token, and config
|
||
// And only if we don't have a valid lastLogin in database
|
||
if (user.id && accessToken && config.keycloakIssuer) {
|
||
try {
|
||
const keycloakLastAccess = await getLastAccessFromKeycloak(user.id, accessToken, config);
|
||
// Use Keycloak last access if available and newer than database value
|
||
if (keycloakLastAccess) {
|
||
if (!lastLogin || keycloakLastAccess > lastLogin) {
|
||
lastLogin = keycloakLastAccess;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// Silently fail and use database value
|
||
console.warn(`⚠️ Could not fetch last access for user ${user.id}, using database value`);
|
||
}
|
||
}
|
||
|
||
return {
|
||
id: user.id,
|
||
namaLengkap: user.namaLengkap,
|
||
namaUser: user.namaUser,
|
||
email: user.email,
|
||
tipeUser: user.tipeUser || '',
|
||
lastLogin: lastLogin, // Will be null if never logged in, otherwise timestamp in seconds
|
||
roles: JSON.parse(user.roles || '[]'),
|
||
realmRoles: JSON.parse(user.realmRoles || '[]'),
|
||
accountRoles: JSON.parse(user.accountRoles || '[]'),
|
||
resourceRoles: JSON.parse(user.resourceRoles || '[]'),
|
||
groups: JSON.parse(user.groups || '[]'),
|
||
given_name: user.given_name,
|
||
family_name: user.family_name,
|
||
createdAt: user.createdAt,
|
||
updatedAt: user.updatedAt,
|
||
};
|
||
}));
|
||
|
||
db.close();
|
||
|
||
console.log(`✅ Retrieved ${formattedUsers.length} users with last access data`);
|
||
return formattedUsers;
|
||
} catch (error: any) {
|
||
console.error("❌ Error fetching users:", error);
|
||
throw createError({
|
||
statusCode: 500,
|
||
statusMessage: error.message || "Failed to fetch users",
|
||
});
|
||
}
|
||
});
|
||
|