Code
·
157 lines
·
5302 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157using MailSharp.MailClient.Models;
using MailSharp.MailClient.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace MailSharp.MailClient.Controllers.Api;
[ApiController]
[Route("api/auth")]
public class AuthApiController(
IAccountStore accountStore,
SessionAccountManager sessionManager,
IImapService imapService,
IOptions<MailSettings> mailSettings,
LocalizationService localizer,
ILogger<AuthApiController> logger) : ControllerBase
{
private readonly MailSettings _settings = mailSettings.Value;
// Must stay reachable while logged out - the login page calls this first to decide whether
// to show the login form or redirect straight to the mail app.
[AllowAnonymous]
[HttpGet("state")]
public IActionResult State()
{
var accounts = sessionManager.GetLoggedInAccountIds()
.Select(id => accountStore.Get(id))
.Where(a => a != null)
.Select(a => AccountDto.From(a!))
.ToList();
var activeId = sessionManager.GetActiveAccountId();
var active = activeId != null ? accountStore.Get(activeId.Value) : null;
return Ok(new AuthStateDto
{
IsLoggedIn = sessionManager.IsLoggedIn,
Account = active != null ? AccountDto.From(active) : null,
Accounts = accounts
});
}
[AllowAnonymous]
[HttpGet("login-defaults")]
public IActionResult LoginDefaults() => Ok(new LoginDefaultsDto
{
ImapHost = _settings.DefaultImapServer,
ImapPort = _settings.DefaultImapPort,
ImapSecurity = _settings.DefaultImapSecurity,
SmtpHost = _settings.DefaultSmtpServer,
SmtpPort = _settings.DefaultSmtpPort,
SmtpSecurity = _settings.DefaultSmtpSecurity
});
// The login call itself must obviously work without already being authenticated.
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest model)
{
if (string.IsNullOrWhiteSpace(model.EmailAddress) || string.IsNullOrWhiteSpace(model.Password))
return BadRequest(new { error = localizer["login_error_email_password_required"] });
var username = string.IsNullOrWhiteSpace(model.Username) ? model.EmailAddress : model.Username;
Account account;
if (model.Advanced)
{
if (string.IsNullOrWhiteSpace(model.ImapHost) || string.IsNullOrWhiteSpace(model.SmtpHost))
return BadRequest(new { error = localizer["login_error_servers_required"] });
account = new Account
{
DisplayName = model.EmailAddress,
EmailAddress = model.EmailAddress,
Username = username,
ImapHost = model.ImapHost,
ImapPort = model.ImapPort,
ImapSecurity = model.ImapSecurity,
SmtpHost = model.SmtpHost,
SmtpPort = model.SmtpPort,
SmtpSecurity = model.SmtpSecurity
};
}
else
{
var domain = model.EmailAddress.Split('@').Last();
account = new Account
{
DisplayName = model.EmailAddress,
EmailAddress = model.EmailAddress,
Username = username,
ImapHost = $"imap.{domain}",
ImapPort = _settings.DefaultImapPort,
ImapSecurity = _settings.DefaultImapSecurity,
SmtpHost = $"smtp.{domain}",
SmtpPort = _settings.DefaultSmtpPort,
SmtpSecurity = _settings.DefaultSmtpSecurity
};
}
// New accounts start with the app-wide defaults (Settings shows real numbers immediately,
// not blank inputs) rather than 0 - see also SettingsApiController.Get, which applies the
// same fallback for accounts created before these settings existed.
account.MessagesPerPage = _settings.MessageListPageSize;
account.ContactsPerPage = _settings.ContactMailsPageSize;
try
{
await imapService.GetFoldersAsync(account, model.Password);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Login connectivity check failed for {EmailAddress} against {ImapHost}", account.EmailAddress, account.ImapHost);
return BadRequest(new { error = localizer.Format("login_error_connect_failed", ex.Message) });
}
var existing = accountStore.GetAll().FirstOrDefault(a =>
a.EmailAddress.Equals(account.EmailAddress, StringComparison.OrdinalIgnoreCase) &&
a.ImapHost.Equals(account.ImapHost, StringComparison.OrdinalIgnoreCase));
// The very first account ever created on a fresh install becomes admin automatically, so
// there's always someone who can reach the Maintenance page without a separate setup step.
if (existing == null && accountStore.GetAll().Count == 0)
{
account.IsAdmin = true;
}
var isNewAccount = existing == null;
existing ??= accountStore.Add(account, model.Password);
await sessionManager.AddOrUpdateAsync(existing.Id);
logger.LogInformation("Account {AccountId} ({EmailAddress}) logged in{NewAccount}", existing.Id, existing.EmailAddress, isNewAccount ? " (new account, admin=" + existing.IsAdmin + ")" : "");
return Ok(AccountDto.From(existing));
}
[HttpPost("switch")]
public async Task<IActionResult> Switch(SwitchAccountRequest request)
{
await sessionManager.SetActiveAccountIdAsync(request.AccountId);
return Ok();
}
[HttpPost("remove")]
public async Task<IActionResult> Remove(RemoveAccountRequest request)
{
await sessionManager.RemoveAsync(request.AccountId);
return Ok(new { isLoggedIn = sessionManager.IsLoggedIn });
}
[HttpPost("logout")]
public async Task<IActionResult> Logout()
{
await sessionManager.LogoutAsync();
return Ok();
}
}