update user login baru dan hakakses
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// server/api/auth/keycloak-callback.ts - EXTENDED SESSION FIX
|
||||
|
||||
// Add this at the top of the file (after imports)
|
||||
const SESSION_DURATION = 24 * 60 * 60; // 7 days in seconds (customize as needed)
|
||||
const SESSION_DURATION = 1 * 60 * 60; // 7 days in seconds (customize as needed)
|
||||
// Or use one of these alternatives:
|
||||
// const SESSION_DURATION = 24 * 60 * 60; // 1 day
|
||||
// const SESSION_DURATION = 30 * 24 * 60 * 60; // 30 days
|
||||
@@ -124,6 +124,17 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
console.log('✅ Session cookie created successfully');
|
||||
|
||||
// Auto-sync user data to database (first time login check)
|
||||
// Pass session createdAt as loginTime to sync function
|
||||
try {
|
||||
const { syncUserFromTokens } = await import('~/server/utils/userSync');
|
||||
const result = syncUserFromTokens(tokens.id_token, tokens.access_token, sessionData.createdAt);
|
||||
console.log(`✅ User auto-sync on login: ${result.action} - ${result.message}`);
|
||||
} catch (syncError: any) {
|
||||
// Don't fail the login if sync fails, just log it
|
||||
console.error('⚠️ Failed to auto-sync user on login:', syncError);
|
||||
}
|
||||
|
||||
const testCookie = getCookie(event, 'user_session');
|
||||
console.log('🧪 Cookie test - can read back in this handler (Expected False):', !!testCookie);
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// server/api/permission.get.ts
|
||||
// Proxy endpoint to fetch permissions from backend API
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log("🔐 Permission endpoint called");
|
||||
|
||||
const query = getQuery(event);
|
||||
const roles = query.roles as string | string[];
|
||||
const groups = query.groups as string | string[];
|
||||
|
||||
if (!roles && !groups) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "roles or groups parameter is required",
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to arrays and handle single values
|
||||
const rolesArray = Array.isArray(roles) ? roles : roles ? [roles] : [];
|
||||
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] || '';
|
||||
|
||||
// Build query parameters
|
||||
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()}`;
|
||||
|
||||
try {
|
||||
console.log(`📡 Fetching permissions from: ${backendUrl}`);
|
||||
console.log(`📋 Query params - roles: ${primaryRole}, groups: ${primaryGroup}`);
|
||||
|
||||
const response = await $fetch(backendUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
console.log("✅ Permission data fetched successfully");
|
||||
console.log("📦 Response structure:", {
|
||||
hasMessage: !!(response as any).message,
|
||||
hasData: !!(response as any).data,
|
||||
dataLength: Array.isArray((response as any).data) ? (response as any).data.length : 0,
|
||||
hasMeta: !!(response as any).meta,
|
||||
});
|
||||
|
||||
// Log first permission item for debugging
|
||||
if ((response as any).data && Array.isArray((response as any).data) && (response as any).data.length > 0) {
|
||||
console.log("📄 Sample permission item:", (response as any).data[0]);
|
||||
}
|
||||
|
||||
// Return the response as-is (it should have { message, data, meta } structure)
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error fetching permissions:", error);
|
||||
console.error("❌ Error details:", {
|
||||
message: error.message,
|
||||
status: error.status || error.statusCode,
|
||||
statusText: error.statusText || error.statusMessage,
|
||||
data: error.data,
|
||||
});
|
||||
|
||||
// Return empty permissions structure if API fails
|
||||
return {
|
||||
message: error.message || "Failed to fetch permissions",
|
||||
data: [],
|
||||
meta: {
|
||||
count: 0,
|
||||
total: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// server/api/users/[id].delete.ts
|
||||
// Delete user
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const getDbPath = () => {
|
||||
const dbDir = join(process.cwd(), 'data');
|
||||
return join(dbDir, 'users.db');
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const userId = getRouterParam(event, 'id');
|
||||
|
||||
console.log(`🗑️ Delete user endpoint called for ID: ${userId}`);
|
||||
|
||||
if (!userId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "User ID is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const dbPath = getDbPath();
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Database not found",
|
||||
});
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(userId) as any;
|
||||
|
||||
if (!existingUser) {
|
||||
db.close();
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Delete user
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(userId);
|
||||
|
||||
db.close();
|
||||
|
||||
console.log(`✅ User deleted: ${userId}`);
|
||||
return { success: true, message: 'User deleted successfully' };
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error deleting user:", error);
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || "Failed to delete user",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// server/api/users/[id].patch.ts
|
||||
// Update user data
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { join } from 'path';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
const getDbPath = () => {
|
||||
const dbDir = join(process.cwd(), 'data');
|
||||
return join(dbDir, 'users.db');
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const userId = getRouterParam(event, 'id');
|
||||
const body = await readBody(event);
|
||||
|
||||
console.log(`🔄 Update user endpoint called for ID: ${userId}`);
|
||||
|
||||
if (!userId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "User ID is required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const dbPath = getDbPath();
|
||||
|
||||
if (!existsSync(dbPath)) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "Database not found",
|
||||
});
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(userId) as any;
|
||||
|
||||
if (!existingUser) {
|
||||
db.close();
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "User not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare update fields
|
||||
const updateFields: string[] = [];
|
||||
const updateValues: any[] = [];
|
||||
|
||||
if (body.namaLengkap !== undefined) {
|
||||
updateFields.push('namaLengkap = ?');
|
||||
updateValues.push(body.namaLengkap);
|
||||
}
|
||||
if (body.tipeUser !== undefined) {
|
||||
updateFields.push('tipeUser = ?');
|
||||
updateValues.push(body.tipeUser);
|
||||
}
|
||||
if (body.lastLogin !== undefined) {
|
||||
updateFields.push('lastLogin = ?');
|
||||
updateValues.push(body.lastLogin);
|
||||
}
|
||||
if (body.roles !== undefined) {
|
||||
updateFields.push('roles = ?');
|
||||
updateValues.push(JSON.stringify(Array.isArray(body.roles) ? body.roles : []));
|
||||
}
|
||||
if (body.realmRoles !== undefined) {
|
||||
updateFields.push('realmRoles = ?');
|
||||
updateValues.push(JSON.stringify(Array.isArray(body.realmRoles) ? body.realmRoles : []));
|
||||
}
|
||||
if (body.accountRoles !== undefined) {
|
||||
updateFields.push('accountRoles = ?');
|
||||
updateValues.push(JSON.stringify(Array.isArray(body.accountRoles) ? body.accountRoles : []));
|
||||
}
|
||||
if (body.resourceRoles !== undefined) {
|
||||
updateFields.push('resourceRoles = ?');
|
||||
updateValues.push(JSON.stringify(Array.isArray(body.resourceRoles) ? body.resourceRoles : []));
|
||||
}
|
||||
if (body.groups !== undefined) {
|
||||
updateFields.push('groups = ?');
|
||||
updateValues.push(JSON.stringify(Array.isArray(body.groups) ? body.groups : []));
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
db.close();
|
||||
return { success: true, message: 'No fields to update' };
|
||||
}
|
||||
|
||||
// Add updatedAt
|
||||
updateFields.push('updatedAt = strftime(\'%s\', \'now\')');
|
||||
updateValues.push(userId);
|
||||
|
||||
// Execute update
|
||||
const sql = `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`;
|
||||
db.prepare(sql).run(...updateValues);
|
||||
|
||||
db.close();
|
||||
|
||||
console.log(`✅ User updated: ${userId}`);
|
||||
return { success: true, message: 'User updated successfully' };
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error updating user:", error);
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || "Failed to update user",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// server/api/users/create.post.ts
|
||||
// Create new user (manual creation)
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { join } from 'path';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
|
||||
const getDbPath = () => {
|
||||
const dbDir = join(process.cwd(), 'data');
|
||||
if (!existsSync(dbDir)) {
|
||||
mkdirSync(dbDir, { recursive: true });
|
||||
}
|
||||
return join(dbDir, 'users.db');
|
||||
};
|
||||
|
||||
const initDb = () => {
|
||||
const dbPath = getDbPath();
|
||||
const db = new Database(dbPath);
|
||||
|
||||
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: Add new columns if they don't exist
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
console.log("➕ Create user endpoint called");
|
||||
|
||||
if (!body.namaLengkap || !body.username) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "namaLengkap and username are required",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const db = initDb();
|
||||
|
||||
// Generate ID if not provided (for manual creation)
|
||||
const userId = body.id || `manual-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Check if username already exists
|
||||
const existingUser = db.prepare('SELECT * FROM users WHERE namaUser = ?').get(body.username) as any;
|
||||
|
||||
if (existingUser) {
|
||||
db.close();
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: "Username already exists",
|
||||
});
|
||||
}
|
||||
|
||||
// Insert new user
|
||||
db.prepare(`
|
||||
INSERT INTO users (
|
||||
id, namaLengkap, namaUser, email, tipeUser, lastLogin,
|
||||
roles, realmRoles, accountRoles, resourceRoles, groups, given_name, family_name
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
userId,
|
||||
body.namaLengkap,
|
||||
body.username,
|
||||
body.email || null,
|
||||
body.tipeUser || '',
|
||||
body.lastLogin || Math.floor(Date.now() / 1000),
|
||||
JSON.stringify(Array.isArray(body.roles) ? body.roles : []),
|
||||
JSON.stringify(Array.isArray(body.realmRoles) ? body.realmRoles : []),
|
||||
JSON.stringify(Array.isArray(body.accountRoles) ? body.accountRoles : []),
|
||||
JSON.stringify(Array.isArray(body.resourceRoles) ? body.resourceRoles : []),
|
||||
JSON.stringify(Array.isArray(body.groups) ? body.groups : []),
|
||||
body.given_name || null,
|
||||
body.family_name || null
|
||||
);
|
||||
|
||||
db.close();
|
||||
|
||||
console.log(`✅ User created: ${userId}`);
|
||||
return {
|
||||
success: true,
|
||||
message: 'User created successfully',
|
||||
id: userId
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error creating user:", error);
|
||||
throw createError({
|
||||
statusCode: error.statusCode || 500,
|
||||
statusMessage: error.message || "Failed to create user",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// server/api/users/current.get.ts
|
||||
// Get current logged-in user data from JWT token
|
||||
|
||||
// Helper function 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) {
|
||||
console.error("❌ Failed to decode token payload:", e);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log("🔍 Current user endpoint called");
|
||||
|
||||
const sessionCookie = getCookie(event, "user_session");
|
||||
|
||||
if (!sessionCookie) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "No session cookie found",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const session = JSON.parse(sessionCookie);
|
||||
|
||||
const isExpired = Date.now() > session.expiresAt;
|
||||
if (isExpired) {
|
||||
deleteCookie(event, "user_session");
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Session expired",
|
||||
});
|
||||
}
|
||||
|
||||
// Decode tokens to get full user data
|
||||
const idTokenPayload = decodeTokenPayload(session.idToken);
|
||||
const accessTokenPayload = decodeTokenPayload(session.accessToken);
|
||||
|
||||
// Extract roles from different sources
|
||||
const realmRoles = accessTokenPayload?.realm_access?.roles ||
|
||||
idTokenPayload?.realm_access?.roles ||
|
||||
[];
|
||||
|
||||
const accountRoles = accessTokenPayload?.resource_access?.account?.roles ||
|
||||
idTokenPayload?.resource_access?.account?.roles ||
|
||||
[];
|
||||
|
||||
// Extract resource roles (from all resources in resource_access)
|
||||
const resourceRoles: string[] = [];
|
||||
if (accessTokenPayload?.resource_access) {
|
||||
Object.keys(accessTokenPayload.resource_access).forEach(resourceName => {
|
||||
if (resourceName !== 'account') { // Exclude account, already handled
|
||||
const resourceRolesArray = accessTokenPayload.resource_access[resourceName]?.roles || [];
|
||||
resourceRoles.push(...resourceRolesArray.map((role: string) => `${resourceName}:${role}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
if (idTokenPayload?.resource_access) {
|
||||
Object.keys(idTokenPayload.resource_access).forEach(resourceName => {
|
||||
if (resourceName !== 'account') { // Exclude account, already handled
|
||||
const resourceRolesArray = idTokenPayload.resource_access[resourceName]?.roles || [];
|
||||
resourceRoles.push(...resourceRolesArray.map((role: string) => `${resourceName}:${role}`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy: Combined roles (for backward compatibility)
|
||||
const roles = [...realmRoles, ...accountRoles, ...resourceRoles];
|
||||
|
||||
// Keycloak uses 'groups_join' in access token, not 'groups'
|
||||
const groups = accessTokenPayload?.groups_join ||
|
||||
accessTokenPayload?.groups ||
|
||||
idTokenPayload?.groups_join ||
|
||||
idTokenPayload?.groups ||
|
||||
[];
|
||||
|
||||
// Determine tipeUser from groups or roles if possible
|
||||
// You can customize this mapping based on your business logic
|
||||
let tipeUser = '';
|
||||
if (Array.isArray(groups) && groups.length > 0) {
|
||||
// Extract tipeUser from groups path (e.g., "/Instalasi STIM/Devops/Superadmin" -> "Superadmin")
|
||||
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 part of path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build user data object
|
||||
const userData = {
|
||||
id: idTokenPayload?.sub || session.user?.id,
|
||||
namaLengkap: idTokenPayload?.name ||
|
||||
session.user?.name ||
|
||||
`${idTokenPayload?.given_name || ''} ${idTokenPayload?.family_name || ''}`.trim() ||
|
||||
idTokenPayload?.preferred_username,
|
||||
namaUser: idTokenPayload?.preferred_username ||
|
||||
session.user?.preferred_username ||
|
||||
idTokenPayload?.email?.split('@')[0],
|
||||
email: idTokenPayload?.email || session.user?.email,
|
||||
given_name: idTokenPayload?.given_name,
|
||||
family_name: idTokenPayload?.family_name,
|
||||
roles: Array.isArray(roles) ? roles : [],
|
||||
realmRoles: Array.isArray(realmRoles) ? realmRoles : [],
|
||||
accountRoles: Array.isArray(accountRoles) ? accountRoles : [],
|
||||
resourceRoles: Array.isArray(resourceRoles) ? resourceRoles : [],
|
||||
groups: Array.isArray(groups) ? groups : [],
|
||||
tipeUser: tipeUser, // Extracted from groups or empty
|
||||
lastLogin: null, // Will be set on sync
|
||||
// Include full token payloads for reference
|
||||
idTokenPayload,
|
||||
accessTokenPayload,
|
||||
};
|
||||
|
||||
console.log("✅ Current user data extracted from JWT");
|
||||
return userData;
|
||||
} catch (parseError: any) {
|
||||
console.error("❌ Failed to parse session or extract user data:", parseError);
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Invalid session data",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// server/api/users/last-access.get.ts
|
||||
// Get last access time from Keycloak Admin API for a specific user using access token from session
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log("🔍 Last access endpoint called");
|
||||
|
||||
const query = getQuery(event);
|
||||
const userId = query.userId as string;
|
||||
|
||||
if (!userId) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
statusMessage: "userId is required",
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
if (!accessToken) {
|
||||
return { lastAccess: null };
|
||||
}
|
||||
|
||||
// Extract realm from issuer (e.g., "http://keycloak:8080/realms/sandbox" -> "sandbox")
|
||||
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 user has no sessions or no permission, return null
|
||||
if (sessionsResponse.status === 404 || sessionsResponse.status === 403) {
|
||||
console.log(`ℹ️ No sessions found or no permission for user ${userId}`);
|
||||
return { lastAccess: null };
|
||||
}
|
||||
return { lastAccess: null };
|
||||
}
|
||||
|
||||
const sessions = await sessionsResponse.json() as any[];
|
||||
|
||||
if (!sessions || sessions.length === 0) {
|
||||
console.log(`ℹ️ No active sessions for user ${userId}`);
|
||||
return { lastAccess: 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)
|
||||
const lastAccess = lastAccessTimestamp > 0 ? Math.floor(lastAccessTimestamp / 1000) : null;
|
||||
|
||||
console.log(`✅ Last access for user ${userId}: ${lastAccess ? new Date(lastAccess * 1000).toISOString() : 'Never'}`);
|
||||
return { lastAccess };
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error fetching last access from Keycloak:", error);
|
||||
// Return null instead of throwing error to allow graceful degradation
|
||||
return { lastAccess: null };
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
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 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) {
|
||||
// No session available, will skip Keycloak fetch
|
||||
console.log("ℹ️ No valid session found, will use database values for last access");
|
||||
}
|
||||
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// 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;
|
||||
|
||||
// Only fetch from Keycloak if we have a valid user ID, access token, and config
|
||||
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
|
||||
if (keycloakLastAccess) {
|
||||
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,
|
||||
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",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// server/api/users/sync.post.ts
|
||||
// Auto-save/update user data when they first login
|
||||
// This endpoint will be called automatically when user logs in
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
console.log("🔄 User sync endpoint called");
|
||||
|
||||
const sessionCookie = getCookie(event, "user_session");
|
||||
|
||||
if (!sessionCookie) {
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "No session cookie found",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const session = JSON.parse(sessionCookie);
|
||||
|
||||
const isExpired = Date.now() > session.expiresAt;
|
||||
if (isExpired) {
|
||||
deleteCookie(event, "user_session");
|
||||
throw createError({
|
||||
statusCode: 401,
|
||||
statusMessage: "Session expired",
|
||||
});
|
||||
}
|
||||
|
||||
// Use the shared sync utility
|
||||
// Use session createdAt as loginTime, or current time if not available
|
||||
const { syncUserFromTokens } = await import('~/server/utils/userSync');
|
||||
const loginTime = session.createdAt || Date.now();
|
||||
const result = syncUserFromTokens(session.idToken, session.accessToken, loginTime);
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error syncing user:", error);
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: error.message || "Failed to sync user data",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user