implementaçao dos scripts para mudar senha e foto, assim como o mockup da tela de mudança

This commit is contained in:
João Pedro Toledo Goncalves 2025-10-16 00:01:51 -03:00
parent 5ccc8653f6
commit d984a5311f
4 changed files with 485 additions and 157 deletions

218
frontend/esqueleto-mvp.html Normal file
View File

@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVP - Minha Conta</title>
<style>
body { font-family: Arial, Helvetica, sans-serif; background-color: #f4f4f9; color: #333; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: center; flex-direction: column; }
.container { background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); max-width: 500px; width: 100%; margin-top: 20px;}
h1 { color: #2c3e50; text-align: center; border-bottom: 2px solid #ecf0f1; padding-bottom: 10px; margin-bottom: 30px; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
input[type="email"], input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 12px;
background-color: #3498db; /* Cor principal azul */
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
margin-top: 20px;
position: relative;
transition: background-color 0.3s;
overflow: hidden; /* Garante que os elementos internos não ultrapassem as bordas arredondadas */
}
button:hover:not(:disabled) { background-color: #2980b9; }
button:disabled { cursor: wait; }
/* Estilos para o preview da imagem */
.photo-upload-container { text-align: center; margin-bottom: 30px; }
#image_preview {
width: 150px;
height: 150px;
border: 3px solid #ecf0f1;
border-radius: 50%;
object-fit: cover;
cursor: pointer;
transition: transform 0.2s;
}
#image_preview:hover { transform: scale(1.05); }
#profile_image_input { display: none; }
/* Estilos para os estados do botão */
.button-content { display: flex; justify-content: center; align-items: center; }
.button-text, .button-loader, .button-success-icon { transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; }
.button-loader {
position: absolute;
opacity: 0;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.5);
border-top-color: #ffffff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.button-success-icon {
position: absolute;
opacity: 0;
font-size: 24px;
line-height: 1;
}
button.sending { background-color: #95a5a6; }
button.sending .button-text { opacity: 0; transform: translateY(-10px); }
button.sending .button-loader { opacity: 1; }
button.success { background-color: #27ae60; }
button.success .button-loader { opacity: 0; }
button.success .button-success-icon { opacity: 1; transform: scale(1.2); }
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<form id="profile-form">
<h1>Minha Conta</h1>
<div class="form-group photo-upload-container">
<img id="image_preview" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150' viewBox='0 0 24 24' fill='%23f0f0f1' stroke='%23ccc' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E" alt="Preview da foto de perfil" title="Clique para selecionar uma nova foto de perfil">
<input type="file" id="profile_image_input" name="ImagePath" accept="image/png, image/jpeg">
</div>
<div class="form-group">
<label for="user_email">E-mail</label>
<input type="email" id="user_email" name="UserEmail" placeholder="seu.usuario@dominio.com" required title="Seu e-mail de login. Não pode ser alterado.">
</div>
<hr style="border: 1px solid #f0f0f0; margin: 30px 0;">
<div class="form-group">
<label for="old_password">Senha Atual</label>
<input type="password" id="old_password" name="OldPassword" placeholder="Necessária para salvar" required title="Sua senha atual é necessária para confirmar qualquer alteração.">
</div>
<div class="form-group">
<label for="new_password">Nova Senha</label>
<input type="password" id="new_password" name="NewPassword" placeholder="Deixe em branco para não alterar" title="Insira sua nova senha. Mínimo de 8 caracteres.">
</div>
<div class="form-group">
<label for="confirm_password">Confirmar Nova Senha</label>
<input type="password" id="confirm_password" name="ConfirmNewPassword" placeholder="Repita a nova senha" title="As senhas devem ser idênticas.">
</div>
<button type="submit" id="submit-button">
<span class="button-content">
<span class="button-text">Salvar Alterações</span>
<span class="button-loader"></span>
<span class="button-success-icon">&#10003;</span>
</span>
</button>
</form>
</div>
<script>
const profileForm = document.getElementById('profile-form');
const imagePreview = document.getElementById('image_preview');
const imageInput = document.getElementById('profile_image_input');
const submitButton = document.getElementById('submit-button');
// A constante agora guarda o SVG embutido para poder restaurá-lo após o refresh.
const placeholderImage = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150' viewBox='0 0 24 24' fill='%23f0f0f1' stroke='%23ccc' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E";
// --- Lógica para o Preview da Imagem ---
imagePreview.addEventListener('click', () => imageInput.click());
imageInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => { imagePreview.src = e.target.result; };
reader.readAsDataURL(file);
}
});
// --- Lógica Unificada de Submissão do Formulário ---
profileForm.addEventListener('submit', (event) => {
event.preventDefault();
// Coleta de dados
const email = document.getElementById('user_email').value;
const oldPassword = document.getElementById('old_password').value;
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
const fileSelected = imageInput.files.length > 0;
// Validações
if (!email || !oldPassword) {
console.warn('VALIDAÇÃO FALHOU: E-mail e Senha Atual são obrigatórios.');
return;
}
if (newPassword && newPassword !== confirmPassword) {
console.warn('VALIDAÇÃO FALHOU: A nova senha e a confirmação não são idênticas.');
return;
}
if (!fileSelected && !newPassword) {
console.info('INFO: Nenhuma alteração detectada (foto ou senha). O envio foi cancelado.');
return;
}
// 1. Inicia a animação de envio
submitButton.classList.add('sending');
submitButton.disabled = true;
console.log(`%c[${new Date().toLocaleTimeString()}] INICIANDO ENVIO...`, 'color: #3498db; font-weight: bold;');
// 2. Simula um delay de rede de 2 segundos
setTimeout(() => {
// Log de simulação no console
const samAccountName = email.split('@')[0];
let actions = [];
if (fileSelected) actions.push({ action: "Atualização de Foto de Perfil", file: imageInput.files[0].name, script: "muda-imagem-perfil.ps1" });
if (newPassword) actions.push({ action: "Alteração de Senha", validation: "SUCESSO (simulado)", script: "muda-senha-usuario.ps1" });
console.group(`%c[${new Date().toLocaleTimeString()}] RESPOSTA DA SIMULAÇÃO DE BACKEND`, 'color: #27ae60; font-weight: bold;');
console.log("DESTINO: API de Atualização de Perfil");
console.log("USUÁRIO:", samAccountName);
console.log("AÇÕES EXECUTADAS:");
console.table(actions);
console.groupEnd();
// 3. Transição para o estado de "sucesso"
submitButton.classList.remove('sending');
submitButton.classList.add('success');
// 4. Aguarda 1.5s no estado de "sucesso" e depois faz o refresh
setTimeout(() => {
submitButton.classList.remove('success');
submitButton.disabled = false;
// Limpa os campos do formulário (refresh)
document.getElementById('new_password').value = '';
document.getElementById('confirm_password').value = '';
document.getElementById('old_password').value = '';
imageInput.value = ''; // Limpa a seleção do arquivo
imagePreview.src = placeholderImage; // Restaura a imagem padrão
console.log(`%c[${new Date().toLocaleTimeString()}] INTERFACE ATUALIZADA E PRONTA.`, 'color: #9b59b6;');
}, 1500);
}, 2000);
});
</script>
</body>
</html>

View File

@ -1,157 +0,0 @@
# cria-usuario-unico.Tests.ps1
# Importa o script que será testado, carregando a função New-UnifiedADUser na memória
. "$PSScriptRoot\cria-usuario-unico.ps1"
# --- SOLUÇÃO DEFINITIVA PARA O ERRO 'CommandNotFoundException' ---
# Em vez de uma função dummy, criamos o comando dinamicamente.
# Isso garante que o comando é registrado oficialmente no PowerShell antes do Pester tentar mocká-lo.
New-Item -Path "function:Enable-Mailbox" -Value { "Comando falso para Pester" } -Force | Out-Null
# Inicia a suíte de testes
Describe 'Testes do Script de Provisionamento New-UnifiedADUser v2.2' {
# --- SETUP INICIAL ---
# Define variáveis e mocks globais para todos os testes
BeforeAll {
# Parâmetros de conexão falsos que serão usados em todos os testes
$script:dummyCred = New-Object System.Management.Automation.PSCredential ('dummyuser', (ConvertTo-SecureString 'dummypass' -AsPlainText -Force))
$script:clientDC = 'client.dc.local'
$script:exchangeDC = 'exchange.dc.local'
# Mocks padrão: por padrão, tudo existe e está disponível
Mock Get-ADUser { return $null }
Mock Get-ADOrganizationalUnit { return $true }
Mock New-ADUser { } -Verifiable
Mock Enable-Mailbox { } -Verifiable # Agora o Pester encontrará este comando para mockar.
Mock Get-ADForest {
return [PSCustomObject]@{
UPNSuffixes = @('cliente.com.br', 'legacy.local')
RootDomain = 'cliente.com.br'
}
}
Mock Write-Log { } -Verifiable
}
# Define os parâmetros de conexão que são obrigatórios para todas as chamadas da função
$connectionParams = @{
ClientADServer = $script:clientDC
ClientADCredential = $script:dummyCred
ExchangeADServer = $script:exchangeDC
ExchangeADCredential = $script:dummyCred
}
Context '1. Validação de Parâmetros Essenciais' {
It '1.1 Deve falhar se um campo crítico como SamAccountName estiver faltando' {
# Prepara os parâmetros do usuário, faltando um campo obrigatório
$userParams = @{
UserPrincipalName = 'teste@cliente.com.br'
GivenName = 'Teste'
Surname = 'Sobrenome'
Name = 'Nome Completo'
Path = 'OU=Teste,DC=local'
}
# Executa a função com os parâmetros de conexão e do usuário
New-UnifiedADUser @connectionParams @userParams
# A função New-ADUser NUNCA deve ser chamada
Assert-VerifiableMocks {
New-ADUser | Should -Not -BeCalled
}
# O log de erro deve ser chamado com a mensagem correta
Assert-VerifiableMocks {
Write-Log -Level 'ERROR' -WithMessage "USUÁRIO IGNORADO: O campo crítico 'SamAccountName' está faltando para ''." | Should -BeCalled
}
}
}
Context '2. Validação de Pré-Voo (Pre-flight Checks)' {
It '2.1 Deve falhar se a OU não existir no AD do Cliente' {
# Simula que a OU não existe APENAS no AD do Cliente
Mock Get-ADOrganizationalUnit -ParameterFilter { $Server -eq $script:clientDC } { return $null }
Mock Get-ADOrganizationalUnit -ParameterFilter { $Server -eq $script:exchangeDC } { return $true }
$userParams = @{ SamAccountName = 'teste.ou1'; UserPrincipalName = 'teste.ou1@cliente.com.br'; GivenName = 'T'; Surname = 'O'; Name = 'TO'; Path = 'OU=Inexistente,DC=local' }
New-UnifiedADUser @connectionParams @userParams
Assert-VerifiableMocks {
New-ADUser | Should -Not -BeCalled
Write-Log -Level 'ERROR' -WithMessage "USUÁRIO IGNORADO: A OU 'OU=Inexistente,DC=local' não foi encontrada no AD do Cliente." | Should -BeCalled
}
}
It '2.2 Deve falhar se a OU não existir no AD do Exchange' {
# Simula que a OU não existe APENAS no AD do Exchange
Mock Get-ADOrganizationalUnit -ParameterFilter { $Server -eq $script:clientDC } { return $true }
Mock Get-ADOrganizationalUnit -ParameterFilter { $Server -eq $script:exchangeDC } { return $null }
$userParams = @{ SamAccountName = 'teste.ou2'; UserPrincipalName = 'teste.ou2@cliente.com.br'; GivenName = 'T'; Surname = 'O'; Name = 'TO'; Path = 'OU=Inexistente,DC=local' }
New-UnifiedADUser @connectionParams @userParams
Assert-VerifiableMocks {
New-ADUser | Should -Not -BeCalled
Write-Log -Level 'ERROR' -WithMessage "USUÁRIO IGNORADO: A OU 'OU=Inexistente,DC=local' não foi encontrada no AD do Exchange." | Should -BeCalled
}
}
It '2.3 Deve falhar se o SamAccountName já existir no AD do Cliente' {
# Simula que o usuário existe APENAS no AD do Cliente
Mock Get-ADUser -ParameterFilter { $Server -eq $script:clientDC -and $Filter -like "*SamAccountName -eq 'usuario.existente'*" } { return $true }
$userParams = @{ SamAccountName = 'usuario.existente'; UserPrincipalName = 'novo.upn@cliente.com.br'; GivenName = 'U'; Surname = 'E'; Name = 'UE'; Path = 'OU=Teste,DC=local' }
New-UnifiedADUser @connectionParams @userParams
Assert-VerifiableMocks {
New-ADUser | Should -Not -BeCalled
Write-Log -Level 'ERROR' -WithMessage "USUÁRIO IGNORADO: O SamAccountName 'usuario.existente' já existe no AD do Cliente." | Should -BeCalled
}
}
It '2.4 Deve falhar se o UserPrincipalName já existir no AD do Exchange' {
# Simula que o UPN existe APENAS no AD do Exchange
Mock Get-ADUser -ParameterFilter { $Server -eq $script:exchangeDC -and $Filter -like "*UserPrincipalName -eq 'upn.existente@cliente.com.br'*" } { return $true }
$userParams = @{ SamAccountName = 'novo.sam'; UserPrincipalName = 'upn.existente@cliente.com.br'; GivenName = 'U'; Surname = 'E'; Name = 'UE'; Path = 'OU=Teste,DC=local' }
New-UnifiedADUser @connectionParams @userParams
Assert-VerifiableMocks {
New-ADUser | Should -Not -BeCalled
Write-Log -Level 'ERROR' -WithMessage "USUÁRIO IGNORADO: O UserPrincipalName 'upn.existente@cliente.com.br' já existe no AD do Exchange." | Should -BeCalled
}
}
It '2.5 Deve falhar se um UPN com sufixo inválido for usado' {
$userParams = @{ SamAccountName = 'teste.upn'; UserPrincipalName = 'teste.upn@dominio.invalido'; GivenName = 'T'; Surname = 'U'; Name = 'TU'; Path = 'OU=Teste,DC=local' }
New-UnifiedADUser @connectionParams @userParams
Assert-VerifiableMocks {
New-ADUser | Should -Not -BeCalled
Write-Log -Level 'ERROR' -WithMessage "USUÁRIO IGNORADO: O sufixo de UPN '@dominio.invalido' não é válido na floresta do AD do Cliente." | Should -BeCalled
}
}
}
Context '3. Sequência de Execução' {
It '3.1 NÃO deve tentar criar a Mailbox se a criação do usuário no AD do Cliente falhar' {
# Força o New-ADUser a lançar uma exceção
Mock New-ADUser { throw "Acesso negado" }
# Garante que todas as validações de pré-voo passem
Mock Get-ADUser { return $null }
Mock Get-ADOrganizationalUnit { return $true }
$userParams = @{ SamAccountName = 'teste.falha'; UserPrincipalName = 'teste.falha@cliente.com.br'; GivenName = 'T'; Surname = 'F'; Name = 'TF'; Path = 'OU=Teste,DC=local' }
New-UnifiedADUser @connectionParams @userParams
# New-ADUser deve ser chamada, mas Enable-Mailbox NUNCA
Assert-VerifiableMocks {
New-ADUser | Should -BeCalled -Exactly 1
Enable-Mailbox | Should -Not -BeCalled
}
}
}
}

View File

@ -0,0 +1,143 @@
<#
.SYNOPSIS
Atualiza a foto de perfil (thumbnailPhoto) de um usuário no Active Directory.
.DESCRIPTION
Versão 1.0. Componente da "Plataforma Unificada de Trabalho Digital", este script é responsável por
processar uma imagem, validá-la, redimensioná-la para um formato adequado e, em seguida,
atualizar o atributo 'thumbnailPhoto' do usuário especificado no Active Directory.
O script inclui validações de tamanho de arquivo para garantir conformidade com os limites do AD.
.NOTES
Autor: Gemini (Especialista NGINX & Automação)
Data: 15 de outubro de 2025
Versão: 1.0
Contexto: Módulo "Atualização de Foto de Perfil" do Portal de Autoatendimento.
#>
function Set-UnifiedADUserPhoto {
[CmdletBinding()]
param(
# --- Parâmetros de Conexão e Autenticação (Obrigatórios) ---
[Parameter(Mandatory = $true, HelpMessage = "IP ou FQDN do Domain Controller principal (cliente).")]
[string]$ClientADServer,
[Parameter(Mandatory = $true, HelpMessage = "Credencial (usuário e senha) da conta de serviço para autenticar no AD do cliente.")]
[pscredential]$ClientADCredential,
# --- Parâmetros do Usuário (Obrigatórios) ---
[Parameter(Mandatory = $true, HelpMessage = "O nome de logon (SamAccountName) do usuário cuja foto será alterada.")]
[string]$SamAccountName,
[Parameter(Mandatory = $true, HelpMessage = "O caminho completo para o arquivo de imagem (JPG/PNG) a ser carregado.")]
[string]$ImagePath,
# --- Parâmetros de Logging ---
[Parameter(Mandatory = $false)]
[string]$LogFilePath = ".\Set-UnifiedADUserPhoto-Log-$(Get-Date -Format 'yyyy-MM-dd').txt"
)
# --- LÓGICA PRINCIPAL ---
# Validação da existência da função de log, para consistência com outros scripts
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
Write-Host "[ERRO FATAL] A função auxiliar 'Write-Log' não foi encontrada. O script não pode continuar." -ForegroundColor Red
return
}
try {
Import-Module ActiveDirectory -ErrorAction Stop
}
catch {
Write-Log -Level 'ERROR' -Message "Falha ao importar o módulo ActiveDirectory. Verifique se as Ferramientas de Administração de Servidor Remoto (RSAT) estão instaladas."
return
}
Write-Log -Level 'INFO' -Message "Iniciando tentativa de atualização de foto para '$SamAccountName'."
# --- Bloco 1: Validação e Processamento da Imagem ---
if (-not (Test-Path -Path $ImagePath -PathType Leaf)) {
Write-Log -Level 'ERROR' -Message "FALHA: O arquivo de imagem especificado não foi encontrado em '$ImagePath'."
Write-Host " [ERRO] O arquivo de imagem não foi encontrado." -ForegroundColor Red
return
}
$imageBytes = $null
try {
Write-Host " Processando imagem de '$ImagePath'..."
# Carrega a biblioteca .NET para manipulação de imagens
Add-Type -AssemblyName System.Drawing
# Carrega a imagem do arquivo
$image = [System.Drawing.Image]::FromFile($ImagePath)
# Define o tamanho máximo em bytes (95 KB para segurança)
$maxSizeInBytes = 97280
# Redimensiona a imagem para um padrão (ex: 240x240 pixels)
$newBitmap = New-Object System.Drawing.Bitmap(240, 240)
$graphics = [System.Drawing.Graphics]::FromImage($newBitmap)
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.DrawImage($image, 0, 0, 240, 240)
# Salva a imagem processada em um fluxo de memória para obter os bytes
$memoryStream = New-Object System.IO.MemoryStream
# Salva como JPEG para melhor compressão
$newBitmap.Save($memoryStream, [System.Drawing.Imaging.ImageFormat]::Jpeg)
$imageBytes = $memoryStream.ToArray()
# Limpeza de recursos para evitar memory leaks
$graphics.Dispose()
$image.Dispose()
$newBitmap.Dispose()
$memoryStream.Dispose()
# Validação final do tamanho
if ($imageBytes.Length -gt $maxSizeInBytes) {
throw "A imagem processada excede o limite de tamanho do AD ($($maxSizeInBytes / 1KB) KB). Tamanho final: $($imageBytes.Length / 1KB) KB."
}
Write-Host " [OK] Imagem processada e validada com sucesso. Tamanho: $($imageBytes.Length / 1KB) KB." -ForegroundColor Green
}
catch {
$errorMessage = "FALHA no processamento da imagem para '$SamAccountName'. Erro: $($_.Exception.Message -replace "`r`n"," ")"
Write-Log -Level 'ERROR' -Message $errorMessage
Write-Host " [ERRO] Falha ao processar a imagem." -ForegroundColor Red
return
}
# --- Bloco 2: Atualização no Active Directory ---
if ($imageBytes) {
try {
Write-Host " Tentando atualizar o atributo 'thumbnailPhoto' no AD do Cliente..."
Set-ADUser -Identity $SamAccountName -Replace @{thumbnailPhoto = $imageBytes} -Server $ClientADServer -Credential $ClientADCredential -ErrorAction Stop
Write-Log -Level 'INFO' -Message "SUCESSO: Foto de perfil para '$SamAccountName' atualizada no AD do Cliente." -TargetADServer $ClientADServer
Write-Host " [OK] Foto de perfil atualizada com sucesso no Active Directory." -ForegroundColor Green
}
catch {
$errorMessage = "FALHA ao atualizar a foto de '$SamAccountName' no AD do Cliente. Erro: $($_.Exception.Message -replace "`r`n"," ")"
Write-Log -Level 'ERROR' -Message $errorMessage -TargetADServer $ClientADServer
Write-Host " [ERRO] Falha ao gravar a imagem no Active Directory." -ForegroundColor Red
return
}
}
Write-Host "Processo de atualização de foto concluído."
}
# --- FUNÇÃO AUXILIAR DE LOG (Exemplo, assumindo que já existe no seu ambiente) ---
# Mantenha sua função de log centralizada. Esta é uma versão mínima para permitir testes autônomos.
function Write-Log {
param(
[Parameter(Mandatory=$true)] [string]$Message,
[Parameter(Mandatory=$true)] [ValidateSet('INFO', 'WARN', 'ERROR')] [string]$Level,
[Parameter(Mandatory=$false)] [string]$TargetADServer
)
$LogFilePath = ".\Set-UnifiedADUserPhoto-Log-$(Get-Date -Format 'yyyy-MM-dd').txt"
$logEntry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - [$Level] - $Message"
$logEntry | Out-File -FilePath $LogFilePath -Append
}

View File

@ -0,0 +1,124 @@
<#
.SYNOPSIS
Altera a senha de um usuário de forma sincronizada em dois ambientes Active Directory (Cliente e Exchange).
.DESCRIPTION
Versão 1.0. Este script é um componente crítico da "Plataforma Unificada de Trabalho Digital".
Ele primeiro valida a senha antiga do usuário e, se bem-sucedido, aplica a nova senha em ambos os Domain Controllers.
O script foi projetado para tratar falhas parciais (ex: sucesso no primeiro AD, falha no segundo)
gerando logs específicos para alertar sobre a dessincronização.
.NOTES
Autor: Gemini (Especialista NGINX & Automação)
Data: 15 de outubro de 2025
Versão: 1.0
Contexto: Módulo "Alteração de Senha (Self-Service)" do Portal de Autoatendimento.
#>
function Set-UnifiedADUserPassword {
[CmdletBinding()]
param(
# --- Parâmetros de Conexão e Autenticação (Obrigatórios) ---
[Parameter(Mandatory = $true, HelpMessage = "IP ou FQDN do Domain Controller principal (cliente).")]
[string]$ClientADServer,
[Parameter(Mandatory = $true, HelpMessage = "Credencial (usuário e senha) da conta de serviço para autenticar no AD do cliente.")]
[pscredential]$ClientADCredential,
[Parameter(Mandatory = $true, HelpMessage = "IP ou FQDN do Domain Controller do ambiente Exchange.")]
[string]$ExchangeADServer,
[Parameter(Mandatory = $true, HelpMessage = "Credencial (usuário e senha) da conta de serviço para autenticar no AD do Exchange.")]
[pscredential]$ExchangeADCredential,
# --- Parâmetros do Usuário (Obrigatórios) ---
[Parameter(Mandatory = $true, HelpMessage = "O nome de logon do usuário (SamAccountName) cuja senha será alterada.")]
[string]$SamAccountName,
[Parameter(Mandatory = $true, HelpMessage = "A senha ANTIGA do usuário. Deve ser um objeto SecureString.")]
[System.Security.SecureString]$OldPassword,
[Parameter(Mandatory = $true, HelpMessage = "A NOVA senha do usuário. Deve ser um objeto SecureString.")]
[System.Security.SecureString]$NewPassword,
# --- Parâmetros de Logging ---
[Parameter(Mandatory = $false)]
[string]$LogFilePath = ".\Set-UnifiedADUserPassword-Log-$(Get-Date -Format 'yyyy-MM-dd').txt"
)
# --- LÓGICA PRINCIPAL ---
# Dependência da função Write-Log (deve estar no mesmo escopo ou módulo)
if (-not (Get-Command Write-Log -ErrorAction SilentlyContinue)) {
Write-Host "[ERRO FATAL] A função auxiliar 'Write-Log' não foi encontrada. O script não pode continuar." -ForegroundColor Red
return
}
try {
Import-Module ActiveDirectory -ErrorAction Stop
}
catch {
Write-Log -Level 'ERROR' -Message "Falha ao importar o módulo ActiveDirectory. Verifique se as Ferramentas de Administração de Servidor Remoto (RSAT) estão instaladas."
return
}
Write-Log -Level 'INFO' -Message "Iniciando tentativa de alteração de senha para '$SamAccountName'."
$clientADSuccess = $false
# --- Bloco 1: Alteração no Active Directory do Cliente ---
# O cmdlet Set-ADAccountPassword valida a senha antiga e define a nova em uma única operação.
try {
Write-Host " Tentando alterar a senha no AD do Cliente..."
Set-ADAccountPassword -Identity $SamAccountName -OldPassword $OldPassword -NewPassword $NewPassword -Server $ClientADServer -Credential $ClientADCredential -ErrorAction Stop
$clientADSuccess = $true
Write-Log -Level 'INFO' -Message "SUCESSO: Senha para '$SamAccountName' alterada no AD do Cliente." -TargetADServer $ClientADServer
Write-Host " [OK] Senha alterada com sucesso no AD do Cliente." -ForegroundColor Green
}
catch {
# A falha aqui geralmente significa "senha antiga incorreta" ou "política de complexidade não atendida".
$errorMessage = "FALHA ao alterar a senha de '$SamAccountName' no AD do Cliente. Erro: $($_.Exception.Message -replace "`r`n"," ")"
Write-Log -Level 'ERROR' -Message $errorMessage -TargetADServer $ClientADServer
Write-Host " [ERRO] Falha ao alterar senha no AD do Cliente. Verifique a senha antiga e as políticas de complexidade." -ForegroundColor Red
# Não continuamos se o primeiro passo falhar.
return
}
# --- Bloco 2: Alteração no Active Directory do Exchange ---
# Este bloco só executa se o primeiro foi bem-sucedido.
if ($clientADSuccess) {
try {
Write-Host " Tentando alterar a senha no AD do Exchange para manter a sincronia..."
# Repetimos a operação no segundo AD
Set-ADAccountPassword -Identity $SamAccountName -OldPassword $OldPassword -NewPassword $NewPassword -Server $ExchangeADServer -Credential $ExchangeADCredential -ErrorAction Stop
Write-Log -Level 'INFO' -Message "SUCESSO: Senha para '$SamAccountName' alterada no AD do Exchange." -TargetADServer $ExchangeADServer
Write-Host " [OK] Senha alterada com sucesso no AD do Exchange." -ForegroundColor Green
Write-Log -Level 'INFO' -Message "SINCRONIZAÇÃO COMPLETA: A senha de '$SamAccountName' foi alterada com sucesso em ambos os ambientes."
}
catch {
# ESTE É O CENÁRIO CRÍTICO DE DESSINCRONIZAÇÃO
$errorMessage = "FALHA DE SINCRONIZAÇÃO: A senha de '$SamAccountName' FOI ALTERADA no AD do Cliente, mas FALHOU no AD do Exchange. AÇÃO MANUAL PODE SER NECESSÁRIA. Erro: $($_.Exception.Message -replace "`r`n"," ")"
Write-Log -Level 'ERROR' -Message $errorMessage -TargetADServer $ExchangeADServer
Write-Host " [ERRO CRÍTICO] Falha ao sincronizar a senha no AD do Exchange. A senha foi alterada apenas no primeiro ambiente!" -ForegroundColor Red
}
}
Write-Host "Processo de alteração de senha concluído."
}
# --- FUNÇÃO AUXILIAR DE LOG (Exemplo, assumindo que já existe no seu ambiente) ---
# Coloque aqui a sua função Write-Log ou carregue-a de outro arquivo .ps1
# Exemplo mínimo para o script funcionar de forma autônoma:
function Write-Log {
param(
[Parameter(Mandatory=$true)] [string]$Message,
[Parameter(Mandatory=$true)] [ValidateSet('INFO', 'WARN', 'ERROR')] [string]$Level,
[Parameter(Mandatory=$false)] [string]$TargetADServer
)
$LogFilePath = ".\Set-UnifiedADUserPassword-Log-$(Get-Date -Format 'yyyy-MM-dd').txt"
$logEntry = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - [$Level] - $Message"
$logEntry | Out-File -FilePath $LogFilePath -Append
}