update user login baru dan hakakses

This commit is contained in:
Fanrouver
2025-12-18 15:11:41 +07:00
parent dfcd59481c
commit c95da96017
20 changed files with 2564 additions and 404 deletions
+54 -7
View File
@@ -51,9 +51,26 @@ export default defineEventHandler(async (event) => {
return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
}
// Validate Keycloak configuration
if (!config.keycloakIssuer) {
console.error('❌ KEYCLOAK_ISSUER is not configured');
const errorMsg = encodeURIComponent('Keycloak server is not configured. Please contact administrator.');
return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
}
if (!config.keycloakClientId || !config.keycloakClientSecret) {
console.error('❌ Keycloak client credentials are not configured');
const errorMsg = encodeURIComponent('Keycloak client credentials are missing. Please contact administrator.');
return sendRedirect(event, `/LoginPage?error=${errorMsg}`);
}
const tokenUrl = `${config.keycloakIssuer}/protocol/openid-connect/token`;
const redirectUri = `${config.public.authUrl}/api/auth/keycloak-callback`;
console.log('🔗 Token URL:', tokenUrl);
console.log('🔗 Redirect URI:', redirectUri);
console.log('🔑 Client ID:', config.keycloakClientId ? '***configured***' : 'MISSING');
const tokenPayload = new URLSearchParams({
grant_type: 'authorization_code',
client_id: config.keycloakClientId,
@@ -62,13 +79,43 @@ export default defineEventHandler(async (event) => {
redirect_uri: redirectUri,
});
const tokenResponse = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: tokenPayload,
});
let tokenResponse;
try {
// Create abort controller for timeout (compatible with all Node.js versions)
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
tokenResponse = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: tokenPayload,
signal: controller.signal,
});
clearTimeout(timeoutId);
} catch (fetchError: any) {
console.error('❌ Fetch error details:');
console.error(' - Error type:', fetchError.name);
console.error(' - Error message:', fetchError.message);
console.error(' - Token URL attempted:', tokenUrl);
// Provide more specific error messages
let errorMsg = 'Failed to connect to authentication server.';
if (fetchError.name === 'AbortError' || fetchError.message.includes('timeout')) {
errorMsg = 'Authentication server timeout. Please try again.';
} else if (fetchError.message.includes('ENOTFOUND') || fetchError.message.includes('getaddrinfo')) {
errorMsg = 'Cannot reach authentication server. Please check network connection.';
} else if (fetchError.message.includes('ECONNREFUSED')) {
errorMsg = 'Authentication server refused connection. Server may be down.';
} else if (fetchError.message.includes('certificate') || fetchError.message.includes('SSL')) {
errorMsg = 'SSL certificate error. Please contact administrator.';
}
const encodedError = encodeURIComponent(errorMsg);
return sendRedirect(event, `/LoginPage?error=${encodedError}`);
}
if (!tokenResponse.ok) {
const errorText = await tokenResponse.text();
+171 -6
View File
@@ -1,5 +1,141 @@
// server/api/permission.get.ts
// Proxy endpoint to fetch permissions from backend API
// Proxy endpoint to fetch permissions from backend API with placeholder fallback
// Placeholder data for testing (matching the example API response)
const PLACEHOLDER_PERMISSIONS: Record<string, any> = {
'superadmin_STIM': {
message: "Data permission berhasil diambil",
data: [
{
id: 1,
create: false,
read: true,
update: false,
disable: false,
delete: false,
active: true,
pagename: "Halaman Utama",
pagesID: 1,
level: 1,
sort: 1
},
{
id: 6,
create: false,
read: true,
update: false,
disable: false,
delete: false,
active: true,
pagename: "Halaman Utama",
pagesID: 1,
level: 1,
sort: 1
},
{
id: 2,
create: false,
read: true,
update: false,
disable: false,
delete: false,
active: true,
pagename: "Pengaturan",
pagesID: 2,
level: 1,
sort: 2
},
{
id: 3,
create: false,
read: true,
update: false,
disable: true,
delete: false,
active: true,
pagename: "Halaman",
pagesID: 3,
level: 2,
sort: 3,
parent: 2
},
{
id: 7,
create: false,
read: true,
update: false,
disable: true,
delete: false,
active: true,
pagename: "Dashboard",
pagesID: 15,
level: 1,
sort: 2
}
],
meta: {
count: 5,
total: 5
}
}
};
// Mapping untuk role dan group yang berbeda
const roleGroupMapping: Record<string, { role: string; group: string }> = {
// Mapping untuk role default-roles-sandbox dengan group Instalasi STIM
'default-roles-sandbox_instalasi stim': { role: 'superadmin', group: 'STIM' },
'default-roles-sandbox_stim': { role: 'superadmin', group: 'STIM' },
// Tambahkan mapping lain jika diperlukan
};
// Normalize group name (remove "Instalasi" prefix if exists)
const normalizeGroup = (group: string): string => {
const normalized = group.trim();
// Jika group mengandung "Instalasi STIM", ambil hanya "STIM"
if (normalized.toLowerCase().includes('instalasi')) {
const parts = normalized.split(/\s+/);
const stimIndex = parts.findIndex(p => p.toLowerCase() === 'stim');
if (stimIndex !== -1) {
return 'STIM';
}
}
// Jika group adalah "Instalasi STIM", return "STIM"
if (normalized.toLowerCase() === 'instalasi stim') {
return 'STIM';
}
return normalized.toUpperCase();
};
// Normalize role name
const normalizeRole = (role: string): string => {
const normalized = role.toLowerCase().trim();
// Mapping khusus untuk role default
if (normalized === 'default-roles-sandbox') {
return 'superadmin';
}
return normalized;
};
// Get placeholder data for a role+group combination
const getPlaceholderData = (role: string, group: string): any | null => {
const normalizedRole = normalizeRole(role);
const normalizedGroup = normalizeGroup(group);
const key = `${normalizedRole}_${normalizedGroup}`;
// Check direct match
let data = PLACEHOLDER_PERMISSIONS[key];
if (data) return data;
// Check mapping
const mappingKey = `${role.toLowerCase()}_${group.toLowerCase()}`;
const mapping = roleGroupMapping[mappingKey];
if (mapping) {
const mappedKey = `${mapping.role.toLowerCase()}_${mapping.group.toUpperCase()}`;
return PLACEHOLDER_PERMISSIONS[mappedKey] || null;
}
return null;
};
export default defineEventHandler(async (event) => {
console.log("🔐 Permission endpoint called");
@@ -8,6 +144,12 @@ export default defineEventHandler(async (event) => {
const roles = query.roles as string | string[];
const groups = query.groups as string | string[];
// Check if placeholder mode is enabled via query parameter or environment variable
// Default to true for testing/development (use placeholder if backend fails)
const forcePlaceholder = query.usePlaceholder === 'true' ||
process.env.USE_PLACEHOLDER_PERMISSIONS === 'true';
const disablePlaceholder = query.usePlaceholder === 'false';
if (!roles && !groups) {
throw createError({
statusCode: 400,
@@ -20,16 +162,32 @@ export default defineEventHandler(async (event) => {
const groupsArray = Array.isArray(groups) ? groups : groups ? [groups] : [];
// Extract primary role and group (use first one or combine)
const primaryRole = rolesArray[0] || '';
const primaryGroup = groupsArray[0] || '';
let primaryRole = rolesArray[0] || '';
let primaryGroup = groupsArray[0] || '';
// Normalize role and group
primaryRole = normalizeRole(primaryRole);
primaryGroup = normalizeGroup(primaryGroup);
console.log(`📋 Normalized params - roles: ${primaryRole}, groups: ${primaryGroup}`);
// Build query parameters
// Check for placeholder data first if placeholder mode is forced
if (forcePlaceholder && !disablePlaceholder) {
const placeholderData = getPlaceholderData(primaryRole, primaryGroup);
if (placeholderData) {
console.log(`📦 Using placeholder data (forced) for role: ${primaryRole}, group: ${primaryGroup}`);
return placeholderData;
}
console.log(`⚠️ No placeholder data found for role: ${primaryRole}, group: ${primaryGroup}`);
}
// Build query parameters (use normalized values for API)
const params = new URLSearchParams();
if (primaryRole) params.append('roles', primaryRole);
if (primaryGroup) params.append('groups', primaryGroup);
// Backend API URL - adjust this to match your backend
const backendUrl = `http://10.10.150.131:8080/api/v1/permission?${params.toString()}`;
const backendUrl = `http://10.10.150.131:8089/api/v1/permission?${params.toString()}`;
try {
console.log(`📡 Fetching permissions from: ${backendUrl}`);
@@ -67,7 +225,14 @@ export default defineEventHandler(async (event) => {
data: error.data,
});
// Return empty permissions structure if API fails
// Fallback to placeholder data if available
const placeholderData = getPlaceholderData(primaryRole, primaryGroup);
if (placeholderData) {
console.log(`📦 Falling back to placeholder data for role: ${primaryRole}, group: ${primaryGroup}`);
return placeholderData;
}
// Return empty permissions structure if API fails and no placeholder available
return {
message: error.message || "Failed to fetch permissions",
data: [],
+133
View File
@@ -35,6 +35,31 @@ export default defineEventHandler(async (event) => {
const db = new Database(dbPath);
// Ensure schema is up to date
try {
const tableInfo = db.prepare("PRAGMA table_info(users)").all() as any[];
const columnNames = tableInfo.map(col => col.name);
if (!columnNames.includes('lastLogin')) {
db.exec(`ALTER TABLE users ADD COLUMN lastLogin INTEGER`);
console.log('✅ Added column: lastLogin');
}
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');
}
} catch (e: any) {
console.warn('Migration note:', e.message);
}
// Check if user exists
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(userId) as any;
@@ -46,6 +71,114 @@ export default defineEventHandler(async (event) => {
});
}
// Handle password update to Keycloak if password is provided
if (body.password && body.password.trim() !== '') {
try {
const config = useRuntimeConfig();
// Get access token from current user session
let accessToken: string | null = null;
try {
const sessionCookie = getCookie(event, "user_session");
if (sessionCookie) {
const session = JSON.parse(sessionCookie);
const isExpired = Date.now() > session.expiresAt;
if (!isExpired && session.accessToken) {
accessToken = session.accessToken;
}
}
} catch (e) {
console.warn("⚠️ No valid session found for password update");
}
if (!accessToken) {
db.close();
throw createError({
statusCode: 401,
statusMessage: "Authentication required to update password",
});
}
// Extract realm from issuer
const issuerUrl = new URL(config.keycloakIssuer);
const realm = issuerUrl.pathname.split('/').filter(Boolean).pop() || 'master';
const keycloakBaseUrl = config.keycloakIssuer.replace('/realms/' + realm, '');
const passwordUpdateUrl = `${keycloakBaseUrl}/admin/realms/${realm}/users/${userId}/reset-password`;
console.log(`🔐 Updating password for user ${userId} in Keycloak`);
console.log(`🔗 Password update URL: ${passwordUpdateUrl}`);
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
// Update password in Keycloak
let passwordResponse;
try {
passwordResponse = await fetch(passwordUpdateUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'password',
value: body.password,
temporary: false, // Set to true if you want to force password change on next login
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
} catch (fetchError: any) {
clearTimeout(timeoutId);
console.error('❌ Fetch error during password update:');
console.error(' - Error type:', fetchError.name);
console.error(' - Error message:', fetchError.message);
console.error(' - URL attempted:', passwordUpdateUrl);
db.close();
let errorMsg = 'Failed to connect to Keycloak server to update password.';
if (fetchError.name === 'AbortError' || fetchError.message.includes('timeout')) {
errorMsg = 'Keycloak server timeout. Please try again.';
} else if (fetchError.message.includes('ENOTFOUND') || fetchError.message.includes('getaddrinfo')) {
errorMsg = 'Cannot reach Keycloak server. Please check network connection.';
} else if (fetchError.message.includes('ECONNREFUSED')) {
errorMsg = 'Keycloak server refused connection. Server may be down.';
} else if (fetchError.message.includes('certificate') || fetchError.message.includes('SSL')) {
errorMsg = 'SSL certificate error. Please contact administrator.';
}
throw createError({
statusCode: 500,
statusMessage: errorMsg,
});
}
if (!passwordResponse.ok) {
const errorText = await passwordResponse.text();
console.error('❌ Keycloak password update failed:', errorText);
db.close();
throw createError({
statusCode: passwordResponse.status,
statusMessage: `Failed to update password in Keycloak: ${errorText}`,
});
}
console.log(`✅ Password updated successfully in Keycloak for user ${userId}`);
} catch (passwordError: any) {
console.error('❌ Error updating password in Keycloak:', passwordError);
db.close();
// Re-throw if it's already a createError
if (passwordError.statusCode) {
throw passwordError;
}
throw createError({
statusCode: 500,
statusMessage: `Failed to update password: ${passwordError.message}`,
});
}
}
// Prepare update fields
const updateFields: string[] = [];
const updateValues: any[] = [];
+37 -10
View File
@@ -17,6 +17,7 @@ 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,
@@ -37,18 +38,44 @@ const initDb = () => {
)
`);
// Migration: Add new columns if they don't exist
// Migration: Check and add missing columns one by one
try {
db.exec(`
ALTER TABLE users ADD COLUMN realmRoles TEXT DEFAULT '[]';
ALTER TABLE users ADD COLUMN accountRoles TEXT DEFAULT '[]';
ALTER TABLE users ADD COLUMN resourceRoles TEXT DEFAULT '[]';
ALTER TABLE users ADD COLUMN lastLogin INTEGER;
`);
} catch (e: any) {
if (!e.message?.includes('duplicate column')) {
console.warn('Migration note:', e.message);
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;
+56 -5
View File
@@ -79,6 +79,47 @@ const getLastAccessFromKeycloak = async (userId: string, accessToken: string, co
}
};
// 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");
@@ -108,23 +149,33 @@ export default defineEventHandler(async (event) => {
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) => {
// Try to get last access from Keycloak, fallback to database value
let lastLogin = user.lastLogin || null;
// 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, otherwise keep database value
// Use Keycloak last access if available and newer than database value
if (keycloakLastAccess) {
lastLogin = keycloakLastAccess;
if (!lastLogin || keycloakLastAccess > lastLogin) {
lastLogin = keycloakLastAccess;
}
}
} catch (error) {
// Silently fail and use database value
@@ -138,7 +189,7 @@ export default defineEventHandler(async (event) => {
namaUser: user.namaUser,
email: user.email,
tipeUser: user.tipeUser || '',
lastLogin: lastLogin,
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 || '[]'),
+401
View File
@@ -0,0 +1,401 @@
// 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");
const sessionCookie = getCookie(event, "user_session");
if (!sessionCookie) {
throw createError({
statusCode: 401,
statusMessage: "No session cookie found",
});
}
try {
const config = useRuntimeConfig();
const session = JSON.parse(sessionCookie);
const isExpired = Date.now() > session.expiresAt;
if (isExpired) {
deleteCookie(event, "user_session");
throw createError({
statusCode: 401,
statusMessage: "Session expired",
});
}
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",
});
}
});
+7
View File
@@ -30,8 +30,15 @@ export default defineEventHandler(async (event) => {
// Use session createdAt as loginTime, or current time if not available
const { syncUserFromTokens } = await import('~/server/utils/userSync');
const loginTime = session.createdAt || Date.now();
console.log("🔄 Syncing user from session...");
console.log(" Session createdAt:", session.createdAt);
console.log(" Login time:", loginTime);
const result = syncUserFromTokens(session.idToken, session.accessToken, loginTime);
console.log(`✅ Sync result: ${result.action} - ${result.message}`);
return result;
} catch (error: any) {
console.error("❌ Error syncing user:", error);