Skip to content

Commit 8bef66e

Browse files
Initial commit: ADloader v1.0.0 AD domain join utility
0 parents  commit 8bef66e

32 files changed

Lines changed: 1682 additions & 0 deletions

.gitignore

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
## .NET / C# Build Output
2+
[Bb]in/
3+
[Oo]bj/
4+
[Dd]ebug/
5+
[Rr]elease/
6+
7+
## Publish artifacts
8+
publish/
9+
10+
## NuGet
11+
*.nupkg
12+
*.snupkg
13+
.nuget/
14+
**/packages/*
15+
!**/packages/build/
16+
17+
## Visual Studio
18+
.vs/
19+
*.suo
20+
*.user
21+
*.userosscache
22+
*.sln.docstates
23+
*.rsuser
24+
25+
## Visual Studio Code
26+
.vscode/
27+
28+
## JetBrains Rider / ReSharper
29+
.idea/
30+
_ReSharper*/
31+
*.[Rr]e[Ss]harper
32+
*.DotSettings.user
33+
34+
## Agent / AI config (local only)
35+
.agents/
36+
.gemini/
37+
38+
## User-specific files
39+
*.userprefs
40+
41+
## Build results
42+
[Dd]ebugPublic/
43+
[Rr]eleases/
44+
x64/
45+
x86/
46+
[Ww][Ii][Nn]32/
47+
[Aa][Rr][Mm]/
48+
[Aa][Rr][Mm]64/
49+
bld/
50+
[Ll]og/
51+
[Ll]ogs/
52+
53+
## Test Results
54+
[Tt]est[Rr]esult*/
55+
[Bb]uild[Ll]og.*
56+
TestResult.xml
57+
58+
## Debug symbols
59+
*.pdb
60+
61+
## Windows-specific
62+
Thumbs.db
63+
ehthumbs.db
64+
Desktop.ini
65+
$RECYCLE.BIN/
66+
67+
## macOS
68+
.DS_Store
69+
.AppleDouble
70+
.LSOverride
71+
72+
## Misc
73+
*.log
74+
*.tmp
75+
*.temp
76+
*.bak
77+
*.swp
78+
*~

ADLoaderLogo.png

406 KB
Loading
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\ADloader.Domain\ADloader.Domain.csproj" />
5+
</ItemGroup>
6+
7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.3" />
9+
</ItemGroup>
10+
11+
<PropertyGroup>
12+
<TargetFramework>net8.0</TargetFramework>
13+
<ImplicitUsings>enable</ImplicitUsings>
14+
<Nullable>enable</Nullable>
15+
</PropertyGroup>
16+
17+
</Project>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace ADloader.Application.Common;
2+
3+
/// <summary>
4+
/// A generic Result class to represent the outcome of business operations.
5+
/// In Clean Architecture, we prefer this over throwing exceptions for business errors.
6+
/// </summary>
7+
public sealed class Result<T>
8+
{
9+
public bool IsSuccess { get; }
10+
public T? Value { get; }
11+
public string? ErrorMessage { get; }
12+
13+
// Constructor is private to enforce using factory methods
14+
private Result(bool isSuccess, T? value, string? errorMessage)
15+
{
16+
IsSuccess = isSuccess;
17+
Value = value;
18+
ErrorMessage = errorMessage;
19+
}
20+
21+
public static Result<T> Success(T value) => new(true, value, null);
22+
public static Result<T> Failure(string errorMessage) => new(false, default, errorMessage);
23+
}
24+
25+
/// <summary>
26+
/// Non-generic version for operations that don't return a value.
27+
/// </summary>
28+
public sealed class Result
29+
{
30+
public bool IsSuccess { get; }
31+
public string? ErrorMessage { get; }
32+
33+
private Result(bool isSuccess, string? errorMessage)
34+
{
35+
IsSuccess = isSuccess;
36+
ErrorMessage = errorMessage;
37+
}
38+
39+
public static Result Success() => new(true, null);
40+
public static Result Failure(string errorMessage) => new(false, errorMessage);
41+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
using ADloader.Domain.Interfaces;
2+
using ADloader.Domain.Models;
3+
using ADloader.Application.Common;
4+
using Microsoft.Extensions.Logging;
5+
using System;
6+
7+
using System.Threading.Tasks;
8+
9+
namespace ADloader.Application.UseCases;
10+
11+
/// <summary>
12+
/// Main orchestrator use case: checks admin rights, network, user existence,
13+
/// optionally creates a user (with confirmation), then joins the local machine to the AD domain.
14+
/// Every step is logged for transparency and diagnostics.
15+
/// </summary>
16+
public class JoinDomainUseCase
17+
{
18+
private readonly IActiveDirectoryService _adService;
19+
private readonly IDomainJoinService _joinService;
20+
private readonly IUserConfirmation _confirmation;
21+
private readonly IEnvironmentService _environment;
22+
private readonly ILogger<JoinDomainUseCase> _logger;
23+
24+
public JoinDomainUseCase(
25+
IActiveDirectoryService adService,
26+
IDomainJoinService joinService,
27+
IUserConfirmation confirmation,
28+
IEnvironmentService environment,
29+
ILogger<JoinDomainUseCase> logger)
30+
{
31+
_adService = adService;
32+
_joinService = joinService;
33+
_confirmation = confirmation;
34+
_environment = environment;
35+
_logger = logger;
36+
}
37+
38+
/// <summary>
39+
/// Executes the full domain-join workflow with pre-flight checks.
40+
/// </summary>
41+
public async Task<Result> ExecuteAsync(DomainJoinRequest request)
42+
{
43+
_logger.LogInformation("══════════════════════════════════════");
44+
_logger.LogInformation(" ADloader v1.0.0 — Начало работы");
45+
_logger.LogInformation("══════════════════════════════════════");
46+
47+
// --- Pre-flight: Input validation ---
48+
if (string.IsNullOrWhiteSpace(request.DomainName))
49+
{
50+
_logger.LogError("Ошибка: имя домена не указано.");
51+
return Result.Failure("Имя домена не может быть пустым.");
52+
}
53+
54+
if (string.IsNullOrWhiteSpace(request.Username))
55+
{
56+
_logger.LogError("Ошибка: имя пользователя не указано.");
57+
return Result.Failure("Имя пользователя не может быть пустым.");
58+
}
59+
60+
if (string.IsNullOrWhiteSpace(request.Password))
61+
{
62+
_logger.LogError("Ошибка: пароль не указан.");
63+
return Result.Failure("Пароль не может быть пустым.");
64+
}
65+
66+
_logger.LogInformation("Домен: {Domain}", request.DomainName);
67+
_logger.LogInformation("Пользователь: {User}", request.Username);
68+
69+
// --- Pre-flight: Check admin rights ---
70+
_logger.LogInformation("[Проверка] Права администратора...");
71+
if (!_environment.IsRunningAsAdmin())
72+
{
73+
_logger.LogError("✗ Приложение запущено БЕЗ прав администратора.");
74+
_logger.LogError(" Запустите ADloader от имени Администратора (правый клик → Запуск от имени администратора).");
75+
return Result.Failure("Для ввода ПК в домен необходимы права администратора. Перезапустите приложение от имени Администратора.");
76+
}
77+
_logger.LogInformation("✓ Права администратора подтверждены.");
78+
79+
// --- Pre-flight: Check network ---
80+
_logger.LogInformation("[Проверка] Сетевое подключение...");
81+
if (!_environment.IsNetworkAvailable())
82+
{
83+
_logger.LogError("✗ Сетевое подключение отсутствует.");
84+
return Result.Failure("Нет сетевого подключения. Проверьте кабель или Wi-Fi.");
85+
}
86+
_logger.LogInformation("✓ Сеть доступна.");
87+
88+
// --- Pre-flight: DNS ping to domain ---
89+
_logger.LogInformation("[Проверка] Доступность домена '{Domain}'...", request.DomainName);
90+
bool domainReachable = await IsDomainReachableAsync(request.DomainName);
91+
if (!domainReachable)
92+
{
93+
_logger.LogWarning("⚠ Не удалось проверить доступность домена по DNS/ping. Продолжаем попытку...");
94+
}
95+
else
96+
{
97+
_logger.LogInformation("✓ Домен '{Domain}' доступен.", request.DomainName);
98+
}
99+
100+
// --- Step 1: Check if user exists ---
101+
_logger.LogInformation("──────────────────────────────────────");
102+
_logger.LogInformation("[Шаг 1] Проверка существования пользователя '{User}'...", request.Username);
103+
try
104+
{
105+
var checkResult = await _adService.UserExistsAsync(
106+
request.DomainName, request.Username, request.Username, request.Password);
107+
108+
if (checkResult.Error != null)
109+
{
110+
_logger.LogWarning("⚠ Ошибка при проверке пользователя: {Error}. Продолжаем...", checkResult.Error);
111+
}
112+
else if (checkResult.Exists)
113+
{
114+
_logger.LogInformation("✓ Пользователь '{User}' найден в домене.", request.Username);
115+
}
116+
else
117+
{
118+
_logger.LogWarning("⚠ Пользователь '{User}' НЕ найден в домене.", request.Username);
119+
120+
// Ask user for confirmation before creating (via DI-injected IUserConfirmation)
121+
bool shouldCreate = await _confirmation.ConfirmAsync(
122+
$"Пользователь '{request.Username}' не найден в домене '{request.DomainName}'.\n\nСоздать нового пользователя?");
123+
124+
if (shouldCreate)
125+
{
126+
_logger.LogInformation("[Шаг 1.1] Создание пользователя '{User}'...", request.Username);
127+
var createResult = await _adService.CreateUserAsync(
128+
request.DomainName, request.Username, request.Password, request.Username, request.Password);
129+
130+
if (createResult.IsSuccess)
131+
{
132+
_logger.LogInformation("✓ Пользователь '{User}' успешно создан.", request.Username);
133+
}
134+
else
135+
{
136+
_logger.LogError("✗ Не удалось создать пользователя: {Error}", createResult.ErrorMessage);
137+
return Result.Failure($"Ошибка создания пользователя: {createResult.ErrorMessage}");
138+
}
139+
}
140+
else
141+
{
142+
_logger.LogInformation("Пользователь отказался от создания учётной записи.");
143+
_logger.LogInformation("Продолжаем попытку ввода в домен с указанными учётными данными...");
144+
}
145+
}
146+
}
147+
catch (Exception ex)
148+
{
149+
_logger.LogWarning("⚠ Не удалось проверить пользователя: {Error}. Продолжаем...", ex.Message);
150+
}
151+
152+
// --- Step 2: Join the domain ---
153+
_logger.LogInformation("──────────────────────────────────────");
154+
_logger.LogInformation("[Шаг 2] Ввод компьютера в домен '{Domain}'...", request.DomainName);
155+
156+
var joinResult = await _joinService.JoinDomainAsync(request);
157+
158+
if (!joinResult.IsSuccess)
159+
{
160+
_logger.LogError("✗ Ошибка ввода в домен: {Error}", joinResult.ErrorMessage);
161+
_logger.LogInformation("──────────────────────────────────────");
162+
_logger.LogInformation("Советы по устранению:");
163+
_logger.LogInformation(" 1. Убедитесь, что ПК подключён к сети домена");
164+
_logger.LogInformation(" 2. Проверьте правильность имени домена (FQDN)");
165+
_logger.LogInformation(" 3. Убедитесь, что логин и пароль верны");
166+
_logger.LogInformation(" 4. Проверьте, что учётная запись имеет права на ввод ПК в домен");
167+
_logger.LogInformation(" 5. Проверьте настройки DNS (должен указывать на контроллер домена)");
168+
return Result.Failure($"Ошибка ввода в домен: {joinResult.ErrorMessage}");
169+
}
170+
171+
_logger.LogInformation("══════════════════════════════════════");
172+
_logger.LogInformation("✓ Компьютер успешно введён в домен '{Domain}'!", request.DomainName);
173+
_logger.LogInformation(" Для применения изменений необходима перезагрузка.");
174+
_logger.LogInformation("══════════════════════════════════════");
175+
176+
return Result.Success();
177+
}
178+
179+
private static async Task<bool> IsDomainReachableAsync(string domainName)
180+
{
181+
try
182+
{
183+
using var ping = new System.Net.NetworkInformation.Ping();
184+
var reply = await ping.SendPingAsync(domainName, 3000);
185+
return reply.Status == System.Net.NetworkInformation.IPStatus.Success;
186+
}
187+
catch
188+
{
189+
return false;
190+
}
191+
}
192+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Threading.Tasks;
2+
3+
namespace ADloader.Domain.Interfaces;
4+
5+
/// <summary>
6+
/// Contract for querying and managing users in Active Directory.
7+
/// </summary>
8+
public interface IActiveDirectoryService
9+
{
10+
/// <summary>
11+
/// Checks if a user with the given sAMAccountName exists in the specified domain.
12+
/// Returns (Exists, Error) — if an error occurs, Exists is false and Error contains the message.
13+
/// </summary>
14+
Task<(bool Exists, string? Error)> UserExistsAsync(string domainName, string samAccountName, string adminUser, string adminPassword);
15+
16+
/// <summary>
17+
/// Creates a new user account in the specified domain.
18+
/// Returns (success, errorMessage).
19+
/// </summary>
20+
Task<(bool IsSuccess, string ErrorMessage)> CreateUserAsync(
21+
string domainName, string samAccountName, string password, string adminUser, string adminPassword);
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using ADloader.Domain.Models;
2+
using System.Threading.Tasks;
3+
4+
namespace ADloader.Domain.Interfaces;
5+
6+
/// <summary>
7+
/// Contract for joining the local machine to an Active Directory domain.
8+
/// Separated from IActiveDirectoryService per the Single Responsibility Principle.
9+
/// </summary>
10+
public interface IDomainJoinService
11+
{
12+
/// <summary>
13+
/// Joins the current machine to the specified AD domain using the provided credentials.
14+
/// </summary>
15+
/// <param name="request">Domain name, username, and password.</param>
16+
/// <returns>A tuple indicating success and an error message on failure.</returns>
17+
Task<(bool IsSuccess, string ErrorMessage)> JoinDomainAsync(DomainJoinRequest request);
18+
}

0 commit comments

Comments
 (0)