245 lines
8.6 KiB
TypeScript
245 lines
8.6 KiB
TypeScript
// 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);
|
|
|
|
// 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;
|
|
|
|
if (!existingUser) {
|
|
db.close();
|
|
throw createError({
|
|
statusCode: 404,
|
|
statusMessage: "User not found",
|
|
});
|
|
}
|
|
|
|
// 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[] = [];
|
|
|
|
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",
|
|
});
|
|
}
|
|
});
|
|
|