update user login baru dan hakakses
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
// server/utils/userSync.ts
|
||||
// Shared utility for syncing user data from JWT tokens to 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');
|
||||
};
|
||||
|
||||
// Initialize database and create table if not exists
|
||||
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 (for existing databases)
|
||||
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) {
|
||||
// Columns might already exist, ignore error
|
||||
if (!e.message?.includes('duplicate column')) {
|
||||
console.warn('Migration note:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Migration: Rename keterangan to lastLogin if exists
|
||||
try {
|
||||
// Check if keterangan column exists
|
||||
const tableInfo = db.prepare("PRAGMA table_info(users)").all() as any[];
|
||||
const hasKeterangan = tableInfo.some(col => col.name === 'keterangan');
|
||||
const hasLastLogin = tableInfo.some(col => col.name === 'lastLogin');
|
||||
|
||||
if (hasKeterangan && !hasLastLogin) {
|
||||
// SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
||||
// For now, we'll just add lastLogin and leave keterangan (it will be ignored)
|
||||
console.log('Migration: Adding lastLogin column');
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.warn('Migration check note:', e.message);
|
||||
}
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
// 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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync user data from JWT tokens to database
|
||||
* @param idToken - The ID token from Keycloak
|
||||
* @param accessToken - The access token from Keycloak
|
||||
* @param loginTime - Optional login timestamp (from session createdAt). If not provided, uses current time
|
||||
* @returns Object with success status and action taken
|
||||
*/
|
||||
export const syncUserFromTokens = (
|
||||
idToken: string,
|
||||
accessToken: string,
|
||||
loginTime?: number
|
||||
): { success: boolean; action: 'created' | 'updated' | 'unchanged'; message: string } => {
|
||||
try {
|
||||
// Decode tokens
|
||||
const idTokenPayload = decodeTokenPayload(idToken);
|
||||
const accessTokenPayload = decodeTokenPayload(accessToken);
|
||||
|
||||
if (!idTokenPayload) {
|
||||
throw new Error('Invalid ID token');
|
||||
}
|
||||
|
||||
// Extract user data
|
||||
const userId = idTokenPayload.sub;
|
||||
const namaLengkap = idTokenPayload.name ||
|
||||
`${idTokenPayload.given_name || ''} ${idTokenPayload.family_name || ''}`.trim() ||
|
||||
idTokenPayload.preferred_username;
|
||||
const namaUser = idTokenPayload.preferred_username ||
|
||||
idTokenPayload.email?.split('@')[0];
|
||||
const email = idTokenPayload.email;
|
||||
|
||||
if (!userId || !namaUser) {
|
||||
throw new Error('Missing required user data (id or username)');
|
||||
}
|
||||
|
||||
// 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
|
||||
// Extract from groups path (e.g., "/Instalasi STIM/Devops/Superadmin" -> "Superadmin")
|
||||
let tipeUser = '';
|
||||
if (Array.isArray(groups) && groups.length > 0) {
|
||||
// Get the last group path and extract the last segment
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no tipeUser from groups, you can also check roles or leave empty for manual entry
|
||||
|
||||
// Use provided loginTime or current time (convert to seconds if in milliseconds)
|
||||
const lastLoginTimestamp = loginTime
|
||||
? (loginTime > 10000000000 ? Math.floor(loginTime / 1000) : loginTime) // Convert ms to seconds if needed
|
||||
: Math.floor(Date.now() / 1000);
|
||||
|
||||
// Initialize database
|
||||
const db = initDb();
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = db.prepare('SELECT * FROM users WHERE id = ?').get(userId) as any;
|
||||
|
||||
const rolesJson = JSON.stringify(Array.isArray(roles) ? roles : []);
|
||||
const realmRolesJson = JSON.stringify(Array.isArray(realmRoles) ? realmRoles : []);
|
||||
const accountRolesJson = JSON.stringify(Array.isArray(accountRoles) ? accountRoles : []);
|
||||
const resourceRolesJson = JSON.stringify(Array.isArray(resourceRoles) ? resourceRoles : []);
|
||||
const groupsJson = JSON.stringify(Array.isArray(groups) ? groups : []);
|
||||
|
||||
if (existingUser) {
|
||||
// User exists - check if data needs updating
|
||||
// Note: tipeUser is only updated if it's empty in database (to preserve manual edits)
|
||||
const needsUpdate =
|
||||
existingUser.namaLengkap !== namaLengkap ||
|
||||
existingUser.namaUser !== namaUser ||
|
||||
existingUser.email !== email ||
|
||||
existingUser.roles !== rolesJson ||
|
||||
existingUser.realmRoles !== realmRolesJson ||
|
||||
existingUser.accountRoles !== accountRolesJson ||
|
||||
existingUser.resourceRoles !== resourceRolesJson ||
|
||||
existingUser.groups !== groupsJson ||
|
||||
existingUser.given_name !== (idTokenPayload.given_name || null) ||
|
||||
existingUser.family_name !== (idTokenPayload.family_name || null) ||
|
||||
(existingUser.tipeUser === '' && tipeUser !== ''); // Only update if empty
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update user data
|
||||
// Only update tipeUser if it's currently empty (preserve manual edits)
|
||||
const updateTipeUser = existingUser.tipeUser === '' ? tipeUser : existingUser.tipeUser;
|
||||
|
||||
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,
|
||||
idTokenPayload.given_name || null,
|
||||
idTokenPayload.family_name || null,
|
||||
updateTipeUser,
|
||||
lastLoginTimestamp, // Update lastLogin timestamp from session
|
||||
userId
|
||||
);
|
||||
|
||||
console.log("✅ User data updated:", userId);
|
||||
db.close();
|
||||
return {
|
||||
success: true,
|
||||
action: 'updated',
|
||||
message: 'User data updated successfully'
|
||||
};
|
||||
} else {
|
||||
console.log("ℹ️ User data unchanged:", userId);
|
||||
db.close();
|
||||
return {
|
||||
success: true,
|
||||
action: 'unchanged',
|
||||
message: 'User data is up to date'
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// New user - insert
|
||||
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,
|
||||
idTokenPayload.given_name || null,
|
||||
idTokenPayload.family_name || null,
|
||||
tipeUser, // tipeUser - extracted from groups or empty
|
||||
lastLoginTimestamp // lastLogin - from session createdAt
|
||||
);
|
||||
|
||||
console.log("✅ New user saved:", userId);
|
||||
db.close();
|
||||
return {
|
||||
success: true,
|
||||
action: 'created',
|
||||
message: 'New user saved successfully'
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ Error syncing user:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user