Files
web-antrean/server/api/users/list.get.ts
T
2026-01-07 07:50:25 +07:00

215 lines
7.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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",
});
}
});