Files
aliasvault/src/AliasVault.Client/Services/CredentialService.cs

347 lines
12 KiB
C#

//-----------------------------------------------------------------------
// <copyright file="CredentialService.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.Client.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using AliasClientDb;
using AliasGenerators.Identity.Implementations;
using AliasGenerators.Identity.Models;
using AliasGenerators.Password;
using AliasGenerators.Password.Implementations;
using AliasVault.Shared.Models;
using Microsoft.EntityFrameworkCore;
using Identity = AliasGenerators.Identity.Models.Identity;
/// <summary>
/// Service class for alias operations.
/// </summary>
public class CredentialService(HttpClient httpClient, DbService dbService, Config config)
{
/// <summary>
/// Generates a random identity for a credential.
/// </summary>
/// <param name="credential">The credential object to update.</param>
/// <returns>Task.</returns>
public async Task<Credential> GenerateRandomIdentity(Credential credential)
{
// Generate a random identity using the IIdentityGenerator implementation.
var identity = await IdentityGeneratorFactory.CreateIdentityGenerator(dbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
// Generate random values for the Identity properties
credential.Username = identity.NickName;
credential.Alias.FirstName = identity.FirstName;
credential.Alias.LastName = identity.LastName;
credential.Alias.NickName = identity.NickName;
credential.Alias.Gender = identity.Gender == Gender.Male ? "Male" : "Female";
credential.Alias.BirthDate = identity.BirthDate;
// Set the email
var emailDomain = GetDefaultEmailDomain();
credential.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}";
// Generate password
credential.Passwords.First().Value = GenerateRandomPassword();
return credential;
}
/// <summary>
/// Gets the default email domain based on settings and available domains.
/// </summary>
/// <returns>Default email domain.</returns>
public string GetDefaultEmailDomain()
{
var defaultDomain = dbService.Settings.DefaultEmailDomain;
// Function to check if a domain is valid
bool IsValidDomain(string domain) =>
!string.IsNullOrEmpty(domain) &&
domain != "DISABLED.TLD" &&
(config.PublicEmailDomains.Contains(domain) || config.PrivateEmailDomains.Contains(domain));
// Get the first valid domain from private or public domains
string GetFirstValidDomain() =>
config.PrivateEmailDomains.Find(IsValidDomain) ??
config.PublicEmailDomains.FirstOrDefault() ??
"example.com";
// Use the default domain if it's valid, otherwise get the first valid domain
string domainToUse = IsValidDomain(defaultDomain) ? defaultDomain : GetFirstValidDomain();
return domainToUse;
}
/// <summary>
/// Generates a random password for a credential.
/// </summary>
/// <returns>Random password.</returns>
public string GenerateRandomPassword()
{
// Generate a random password using a IPasswordGenerator implementation.
IPasswordGenerator passwordGenerator = new SpamOkPasswordGenerator();
return passwordGenerator.GenerateRandomPassword();
}
/// <summary>
/// Generate random identity by calling the IdentityGenerator API.
/// </summary>
/// <returns>Identity object.</returns>
public async Task<Identity> GenerateRandomIdentityAsync()
{
var identity = await httpClient.GetFromJsonAsync<Identity>("api/v1/Identity/Generate");
if (identity is null)
{
throw new InvalidOperationException("Failed to generate random identity.");
}
return identity;
}
/// <summary>
/// Insert new entry into database.
/// </summary>
/// <param name="loginObject">Login object to insert.</param>
/// <param name="saveToDb">Whether to commit changes to database. Defaults to true, but can be set to false if entries are added in bulk by caller.</param>
/// <returns>Guid of inserted entry.</returns>
public async Task<Guid> InsertEntryAsync(Credential loginObject, bool saveToDb = true)
{
var context = await dbService.GetDbContextAsync();
// Try to extract favicon from service URL
await ExtractFaviconAsync(loginObject);
var login = new Credential
{
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
Notes = loginObject.Notes,
Username = loginObject.Username,
Alias = new Alias()
{
NickName = loginObject.Alias.NickName,
FirstName = loginObject.Alias.FirstName,
LastName = loginObject.Alias.LastName,
BirthDate = loginObject.Alias.BirthDate,
Gender = loginObject.Alias.Gender,
Email = loginObject.Alias.Email,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
},
Service = new Service()
{
Name = loginObject.Service.Name,
Url = loginObject.Service.Url,
Logo = loginObject.Service.Logo,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow,
},
};
login.Passwords.Add(new Password()
{
Value = loginObject.Passwords.First().Value,
});
foreach (var attachment in loginObject.Attachments)
{
login.Attachments.Add(attachment);
}
await context.Credentials.AddAsync(login);
// Add password.
login.Passwords.Add(loginObject.Passwords.First());
if (saveToDb)
{
await dbService.SaveDatabaseAsync();
}
return login.Id;
}
/// <summary>
/// Update an existing entry to database.
/// </summary>
/// <param name="loginObject">Login object to update.</param>
/// <returns>Guid of updated entry.</returns>
public async Task<Guid> UpdateEntryAsync(Credential loginObject)
{
var context = await dbService.GetDbContextAsync();
// Try to extract favicon from service URL
await ExtractFaviconAsync(loginObject);
// Get the existing entry.
var login = await LoadEntryAsync(loginObject.Id);
if (login is null)
{
throw new InvalidOperationException("Login object not found.");
}
login.UpdatedAt = DateTime.UtcNow;
login.Notes = loginObject.Notes;
login.Username = loginObject.Username;
login.Alias.NickName = loginObject.Alias.NickName;
login.Alias.FirstName = loginObject.Alias.FirstName;
login.Alias.LastName = loginObject.Alias.LastName;
login.Alias.BirthDate = loginObject.Alias.BirthDate;
login.Alias.Gender = loginObject.Alias.Gender;
login.Alias.Email = loginObject.Alias.Email;
login.Passwords = loginObject.Passwords;
login.Service.Name = loginObject.Service.Name;
login.Service.Url = loginObject.Service.Url;
login.Service.Logo = loginObject.Service.Logo;
login.Service.UpdatedAt = DateTime.UtcNow;
// Remove attachments that are no longer in the list
var existingAttachments = login.Attachments.ToList();
foreach (var existingAttachment in existingAttachments)
{
if (!loginObject.Attachments.Any(a => a.Id != Guid.Empty && a.Id == existingAttachment.Id))
{
context.Entry(existingAttachment).State = EntityState.Deleted;
}
}
// Add new attachments
foreach (var attachment in loginObject.Attachments)
{
if (!login.Attachments.Any(a => attachment.Id != Guid.Empty && a.Id == attachment.Id))
{
login.Attachments.Add(attachment);
}
else
{
context.Entry(attachment).State = EntityState.Modified;
}
}
await dbService.SaveDatabaseAsync();
return login.Id;
}
/// <summary>
/// Load existing entry from database.
/// </summary>
/// <param name="loginId">Id of login to load.</param>
/// <returns>Alias object.</returns>
public async Task<Credential?> LoadEntryAsync(Guid loginId)
{
var context = await dbService.GetDbContextAsync();
var loginObject = await context.Credentials
.Include(x => x.Passwords)
.Include(x => x.Alias)
.Include(x => x.Service)
.Include(x => x.Attachments)
.AsSplitQuery()
.Where(x => x.Id == loginId)
.FirstOrDefaultAsync();
return loginObject;
}
/// <summary>
/// Load all entries from database.
/// </summary>
/// <returns>Alias object.</returns>
public async Task<List<Credential>> LoadAllAsync()
{
var context = await dbService.GetDbContextAsync();
var loginObject = await context.Credentials
.Include(x => x.Passwords)
.Include(x => x.Alias)
.Include(x => x.Service)
.Include(x => x.Attachments)
.AsSplitQuery()
.ToListAsync();
return loginObject;
}
/// <summary>
/// Get list with all login entries.
/// </summary>
/// <returns>List of CredentialListEntry objects.</returns>
public async Task<List<CredentialListEntry>?> GetListAsync()
{
var context = await dbService.GetDbContextAsync();
// Retrieve all aliases from client DB.
return await context.Credentials
.Include(x => x.Alias)
.Include(x => x.Service)
.AsSplitQuery()
.Select(x => new CredentialListEntry
{
Id = x.Id,
Logo = x.Service.Logo,
Service = x.Service.Name,
CreateDate = x.CreatedAt,
})
.ToListAsync();
}
/// <summary>
/// Removes existing entry from database.
/// </summary>
/// <param name="id">Id of alias to delete.</param>
/// <returns>Task.</returns>
public async Task DeleteEntryAsync(Guid id)
{
var context = await dbService.GetDbContextAsync();
var login = await context.Credentials
.Where(x => x.Id == id)
.FirstAsync();
context.Credentials.Remove(login);
await context.SaveChangesAsync();
await dbService.SaveDatabaseAsync();
}
/// <summary>
/// Extract favicon from service URL if available in object. If successful the passed object itself will be updated with the bytes.
/// </summary>
/// <param name="credentialObject">The Credential object to extract the favicon for.</param>
/// <returns>Task.</returns>
private async Task ExtractFaviconAsync(Credential credentialObject)
{
// Try to extract favicon from service URL
var url = credentialObject.Service.Url;
if (url != null && !string.IsNullOrEmpty(url) && url.Contains("http"))
{
// Request favicon from from service URL via WebApi
try
{
var apiReturn =
await httpClient.GetFromJsonAsync<FaviconExtractModel>($"api/v1/Favicon/Extract?url={url}");
if (apiReturn != null && apiReturn.Image != null)
{
credentialObject.Service.Logo = apiReturn.Image;
}
}
catch
{
// Ignore favicon extraction errors
}
}
}
}