update user login baru dan hakakses

This commit is contained in:
Fanrouver
2025-12-16 10:42:45 +07:00
parent 78de0418e1
commit d2a51f3aee
24 changed files with 2606 additions and 189 deletions
+299
View File
@@ -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;
}
};