mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-24 06:39:12 -05:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18978b94be | ||
|
|
c989573565 | ||
|
|
67ce7da21a | ||
|
|
fb2972695a | ||
|
|
2f47f81af8 | ||
|
|
6d6ee8bf3f | ||
|
|
881eb58a35 | ||
|
|
80bc7cd223 | ||
|
|
87f494fea8 | ||
|
|
a24e533e4c | ||
|
|
ebb8b27f85 | ||
|
|
41c210e75a | ||
|
|
2a50a455d8 | ||
|
|
6896c4cd1d | ||
|
|
9560572a40 | ||
|
|
4dffb9c3c0 | ||
|
|
b8cb3c4d78 | ||
|
|
6f54b05d5a | ||
|
|
d051d69aea | ||
|
|
02f0c43cbd | ||
|
|
14cce42091 | ||
|
|
a1c26cec04 | ||
|
|
42fc1c018c | ||
|
|
f3e740bab3 | ||
|
|
bbdf47d6f4 | ||
|
|
5faf93d6be | ||
|
|
fa1573ee13 | ||
|
|
50f7866a0b | ||
|
|
7b1a1e893e | ||
|
|
40afea3908 | ||
|
|
e1ae260fc5 | ||
|
|
c33399b91d | ||
|
|
f46202223a | ||
|
|
0867573f2f | ||
|
|
2becb3aa8f | ||
|
|
dc2f4dd040 | ||
|
|
2cf3c142da | ||
|
|
a8d84fd38a | ||
|
|
4a207763cc | ||
|
|
b1ef5c33db | ||
|
|
578532efdf | ||
|
|
95fb8baaaa | ||
|
|
73e432b2dc | ||
|
|
f43c3171b0 |
@@ -27,4 +27,15 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Identity\Implementations\Dictionaries\en\firstnames_female" />
|
||||
<EmbeddedResource Include="Identity\Implementations\Dictionaries\en\firstnames_male" />
|
||||
<EmbeddedResource Include="Identity\Implementations\Dictionaries\en\lastnames" />
|
||||
<EmbeddedResource Include="Identity\Implementations\Dictionaries\nl\firstnames_female" />
|
||||
<None Remove="Identity\Implementations\Lists\nl\firstnames" />
|
||||
<EmbeddedResource Include="Identity\Implementations\Dictionaries\nl\firstnames_male" />
|
||||
<None Remove="Identity\Implementations\Lists\nl\lastnames" />
|
||||
<EmbeddedResource Include="Identity\Implementations\Dictionaries\nl\lastnames" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasGenerators.Identity;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IdentityGenerator.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 AliasGenerators.Identity.Implementations.Base;
|
||||
|
||||
using System.Reflection;
|
||||
using AliasGenerators.Identity;
|
||||
using AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract identity generator which implements IIdentityGenerator and generates
|
||||
/// random identities for a certain language.
|
||||
/// </summary>
|
||||
public abstract class IdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// List of male first names in memory.
|
||||
/// </summary>
|
||||
private readonly List<string> _firstNamesMale;
|
||||
|
||||
/// <summary>
|
||||
/// List of female first names in memory.
|
||||
/// </summary>
|
||||
private readonly List<string> _firstNamesFemale;
|
||||
|
||||
/// <summary>
|
||||
/// List of last names in memory.
|
||||
/// </summary>
|
||||
private readonly List<string> _lastNames;
|
||||
|
||||
/// <summary>
|
||||
/// Random instance.
|
||||
/// </summary>
|
||||
private readonly Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IdentityGenerator"/> class.
|
||||
/// </summary>
|
||||
protected IdentityGenerator()
|
||||
{
|
||||
_firstNamesMale = LoadList(FirstNamesListMale);
|
||||
_firstNamesFemale = LoadList(FirstNamesListFemale);
|
||||
_lastNames = LoadList(LastNamesList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets namespace path to the male first names list for the correct language.
|
||||
/// </summary>
|
||||
protected virtual string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_male";
|
||||
|
||||
/// <summary>
|
||||
/// Gets namespace path to the female first names list for the correct language.
|
||||
/// </summary>
|
||||
protected virtual string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_female";
|
||||
|
||||
/// <summary>
|
||||
/// Gets namespace path to the last names list for the correct language.
|
||||
/// </summary>
|
||||
protected virtual string LastNamesList => "AliasGenerators.Identity.Implementations.Dictionaries.nl.lastnames";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Identity> GenerateRandomIdentityAsync()
|
||||
{
|
||||
await Task.Yield(); // Add an await statement to make the method truly asynchronous.
|
||||
|
||||
// Generate identity.
|
||||
var identity = new Identity();
|
||||
|
||||
// Determine gender.
|
||||
if (_random.Next(2) == 0)
|
||||
{
|
||||
identity.FirstName = _firstNamesMale[_random.Next(_firstNamesMale.Count)];
|
||||
identity.Gender = Gender.Male;
|
||||
}
|
||||
else
|
||||
{
|
||||
identity.FirstName = _firstNamesFemale[_random.Next(_firstNamesFemale.Count)];
|
||||
identity.Gender = Gender.Female;
|
||||
}
|
||||
|
||||
identity.LastName = _lastNames[_random.Next(_lastNames.Count)];
|
||||
|
||||
// Generate random date of birth between 21 and 65 years of age.
|
||||
identity.BirthDate = GenerateRandomDateOfBirth();
|
||||
identity.EmailPrefix = new UsernameEmailGenerator().GenerateEmailPrefix(identity);
|
||||
identity.NickName = new UsernameEmailGenerator().GenerateUsername(identity);
|
||||
|
||||
return identity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a list of words from a resource file.
|
||||
/// </summary>
|
||||
/// <param name="resourceName">Name of the resource file to load.</param>
|
||||
/// <returns>List of words from the resource file.</returns>
|
||||
/// <exception cref="FileNotFoundException">Thrown if resource file cannot be found.</exception>
|
||||
private static List<string> LoadList(string resourceName)
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
throw new FileNotFoundException("Resource '" + resourceName + "' not found.", resourceName);
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var words = new List<string>();
|
||||
while (!reader.EndOfStream)
|
||||
{
|
||||
var line = reader.ReadLine();
|
||||
if (line != null)
|
||||
{
|
||||
words.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random date of birth.
|
||||
/// </summary>
|
||||
/// <returns>DateTime representing date of birth.</returns>
|
||||
private DateTime GenerateRandomDateOfBirth()
|
||||
{
|
||||
// Generate random date of birth between 21 and 65 years of age.
|
||||
var now = DateTime.Now;
|
||||
var minDob = now.AddYears(-65);
|
||||
var maxDob = now.AddYears(-21);
|
||||
return minDob.AddDays(_random.Next((int)(maxDob - minDob).TotalDays));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
Emily
|
||||
Emma
|
||||
Olivia
|
||||
Ava
|
||||
Sophia
|
||||
Isabella
|
||||
Mia
|
||||
Charlotte
|
||||
Amelia
|
||||
Harper
|
||||
Evelyn
|
||||
Abigail
|
||||
Elizabeth
|
||||
Sofia
|
||||
Avery
|
||||
Ella
|
||||
Madison
|
||||
Scarlett
|
||||
Victoria
|
||||
Aria
|
||||
Grace
|
||||
Chloe
|
||||
Camila
|
||||
Penelope
|
||||
Riley
|
||||
Layla
|
||||
Zoey
|
||||
Nora
|
||||
Lily
|
||||
Eleanor
|
||||
Hannah
|
||||
Lillian
|
||||
Addison
|
||||
Aubrey
|
||||
Ellie
|
||||
Stella
|
||||
Natalie
|
||||
Zoe
|
||||
Leah
|
||||
Hazel
|
||||
Violet
|
||||
Aurora
|
||||
Savannah
|
||||
Audrey
|
||||
Brooklyn
|
||||
Bella
|
||||
Claire
|
||||
Skylar
|
||||
Lucy
|
||||
Paisley
|
||||
Everly
|
||||
Anna
|
||||
Caroline
|
||||
Nova
|
||||
Genesis
|
||||
Emilia
|
||||
Kennedy
|
||||
Samantha
|
||||
Maya
|
||||
Willow
|
||||
Kinsley
|
||||
Naomi
|
||||
Aaliyah
|
||||
Elena
|
||||
Sarah
|
||||
Ariana
|
||||
Allison
|
||||
Gabriella
|
||||
Alice
|
||||
Madelyn
|
||||
Cora
|
||||
Ruby
|
||||
Eva
|
||||
Serenity
|
||||
Autumn
|
||||
Adeline
|
||||
Hailey
|
||||
Gianna
|
||||
Valentina
|
||||
Isla
|
||||
Eliana
|
||||
Quinn
|
||||
Nevaeh
|
||||
Ivy
|
||||
Sadie
|
||||
Piper
|
||||
Lydia
|
||||
Alexa
|
||||
Josephine
|
||||
Emery
|
||||
Julia
|
||||
Delilah
|
||||
Arianna
|
||||
Vivian
|
||||
Kaylee
|
||||
Sophie
|
||||
Brielle
|
||||
Madeline
|
||||
Peyton
|
||||
Rylee
|
||||
Clara
|
||||
Hadley
|
||||
Melanie
|
||||
Mackenzie
|
||||
Reagan
|
||||
Adalyn
|
||||
Liliana
|
||||
Aubree
|
||||
Jade
|
||||
Katherine
|
||||
Isabelle
|
||||
Natalia
|
||||
Raelynn
|
||||
Maria
|
||||
Athena
|
||||
Ximena
|
||||
Arya
|
||||
Leilani
|
||||
Taylor
|
||||
Faith
|
||||
Rose
|
||||
Kylie
|
||||
Alexandra
|
||||
Mary
|
||||
Margaret
|
||||
Lyla
|
||||
Ashley
|
||||
Amaya
|
||||
Eliza
|
||||
Brianna
|
||||
Bailey
|
||||
Andrea
|
||||
Khloe
|
||||
Jasmine
|
||||
Melody
|
||||
Iris
|
||||
Isabel
|
||||
Norah
|
||||
Annabelle
|
||||
Valeria
|
||||
Emerson
|
||||
Adalynn
|
||||
Ryleigh
|
||||
Eden
|
||||
Emersyn
|
||||
Anastasia
|
||||
Kayla
|
||||
Alyssa
|
||||
Anna
|
||||
Juliana
|
||||
Charlie
|
||||
Lucia
|
||||
Stella
|
||||
@@ -0,0 +1,142 @@
|
||||
Michael
|
||||
Christopher
|
||||
Matthew
|
||||
Joshua
|
||||
Daniel
|
||||
David
|
||||
Andrew
|
||||
Joseph
|
||||
James
|
||||
John
|
||||
Robert
|
||||
William
|
||||
Ryan
|
||||
Jason
|
||||
Nicholas
|
||||
Jonathan
|
||||
Jacob
|
||||
Brandon
|
||||
Tyler
|
||||
Zachary
|
||||
Kevin
|
||||
Justin
|
||||
Benjamin
|
||||
Anthony
|
||||
Samuel
|
||||
Thomas
|
||||
Alexander
|
||||
Ethan
|
||||
Noah
|
||||
Dylan
|
||||
Nathan
|
||||
Christian
|
||||
Austin
|
||||
Adam
|
||||
Caleb
|
||||
Cody
|
||||
Jordan
|
||||
Logan
|
||||
Aaron
|
||||
Kyle
|
||||
Jose
|
||||
Brian
|
||||
Gabriel
|
||||
Timothy
|
||||
Luke
|
||||
Jared
|
||||
Connor
|
||||
Sean
|
||||
Evan
|
||||
Isaac
|
||||
Jack
|
||||
Cameron
|
||||
Hunter
|
||||
Jackson
|
||||
Charles
|
||||
Devin
|
||||
Stephen
|
||||
Patrick
|
||||
Steven
|
||||
Elijah
|
||||
Scott
|
||||
Mark
|
||||
Jeffrey
|
||||
Corey
|
||||
Juan
|
||||
Luis
|
||||
Derek
|
||||
Chase
|
||||
Travis
|
||||
Alex
|
||||
Spencer
|
||||
Ian
|
||||
Trevor
|
||||
Bryan
|
||||
Tanner
|
||||
Marcus
|
||||
Jeremy
|
||||
Eric
|
||||
Jaden
|
||||
Garrett
|
||||
Isaiah
|
||||
Dustin
|
||||
Jesse
|
||||
Seth
|
||||
Blake
|
||||
Nathaniel
|
||||
Mason
|
||||
Liam
|
||||
Paul
|
||||
Carlos
|
||||
Mitchell
|
||||
Parker
|
||||
Lucas
|
||||
Richard
|
||||
Cole
|
||||
Adrian
|
||||
Colin
|
||||
Bradley
|
||||
Jesus
|
||||
Peter
|
||||
Kenneth
|
||||
Joel
|
||||
Victor
|
||||
Bryce
|
||||
Casey
|
||||
Vincent
|
||||
Edward
|
||||
Henry
|
||||
Dominic
|
||||
Riley
|
||||
Shane
|
||||
Dalton
|
||||
Grant
|
||||
Shawn
|
||||
Braden
|
||||
Caden
|
||||
Max
|
||||
Hayden
|
||||
Owen
|
||||
Brett
|
||||
Trevor
|
||||
Philip
|
||||
Brendan
|
||||
Wesley
|
||||
Aidan
|
||||
Brady
|
||||
Colton
|
||||
Tristan
|
||||
George
|
||||
Gavin
|
||||
Dawson
|
||||
Miguel
|
||||
Antonio
|
||||
Nolan
|
||||
Dakota
|
||||
Jace
|
||||
Collin
|
||||
Preston
|
||||
Levi
|
||||
Alan
|
||||
Jorge
|
||||
Carson
|
||||
@@ -0,0 +1,167 @@
|
||||
Smith
|
||||
Johnson
|
||||
Williams
|
||||
Brown
|
||||
Jones
|
||||
Garcia
|
||||
Miller
|
||||
Davis
|
||||
Rodriguez
|
||||
Martinez
|
||||
Hernandez
|
||||
Lopez
|
||||
Gonzalez
|
||||
Wilson
|
||||
Anderson
|
||||
Thomas
|
||||
Taylor
|
||||
Moore
|
||||
Jackson
|
||||
Martin
|
||||
Lee
|
||||
Perez
|
||||
Thompson
|
||||
White
|
||||
Harris
|
||||
Sanchez
|
||||
Clark
|
||||
Ramirez
|
||||
Lewis
|
||||
Robinson
|
||||
Walker
|
||||
Young
|
||||
Allen
|
||||
King
|
||||
Wright
|
||||
Scott
|
||||
Torres
|
||||
Nguyen
|
||||
Hill
|
||||
Flores
|
||||
Green
|
||||
Adams
|
||||
Nelson
|
||||
Baker
|
||||
Hall
|
||||
Rivera
|
||||
Campbell
|
||||
Mitchell
|
||||
Carter
|
||||
Roberts
|
||||
Gomez
|
||||
Phillips
|
||||
Evans
|
||||
Turner
|
||||
Diaz
|
||||
Parker
|
||||
Cruz
|
||||
Edwards
|
||||
Collins
|
||||
Reyes
|
||||
Stewart
|
||||
Morris
|
||||
Morales
|
||||
Murphy
|
||||
Cook
|
||||
Rogers
|
||||
Gutierrez
|
||||
Ortiz
|
||||
Morgan
|
||||
Cooper
|
||||
Peterson
|
||||
Bailey
|
||||
Reed
|
||||
Kelly
|
||||
Howard
|
||||
Ramos
|
||||
Kim
|
||||
Cox
|
||||
Ward
|
||||
Richardson
|
||||
Watson
|
||||
Brooks
|
||||
Chavez
|
||||
Wood
|
||||
James
|
||||
Bennett
|
||||
Gray
|
||||
Mendoza
|
||||
Ruiz
|
||||
Hughes
|
||||
Price
|
||||
Alvarez
|
||||
Castillo
|
||||
Sanders
|
||||
Patel
|
||||
Myers
|
||||
Long
|
||||
Ross
|
||||
Foster
|
||||
Jimenez
|
||||
Powell
|
||||
Jenkins
|
||||
Perry
|
||||
Russell
|
||||
Sullivan
|
||||
Bell
|
||||
Coleman
|
||||
Butler
|
||||
Henderson
|
||||
Barnes
|
||||
Gonzales
|
||||
Fisher
|
||||
Vasquez
|
||||
Simmons
|
||||
Romero
|
||||
Jordan
|
||||
Patterson
|
||||
Alexander
|
||||
Hamilton
|
||||
Graham
|
||||
Reynolds
|
||||
Griffin
|
||||
Wallace
|
||||
Moreno
|
||||
West
|
||||
Cole
|
||||
Hayes
|
||||
Bryant
|
||||
Herrera
|
||||
Gibson
|
||||
Ellis
|
||||
Tran
|
||||
Medina
|
||||
Aguilar
|
||||
Stevens
|
||||
Murray
|
||||
Ford
|
||||
Castro
|
||||
Marshall
|
||||
Owens
|
||||
Harrison
|
||||
Fernandez
|
||||
McDonald
|
||||
Woods
|
||||
Washington
|
||||
Kennedy
|
||||
Wells
|
||||
Vargas
|
||||
Henry
|
||||
Chen
|
||||
Freeman
|
||||
Webb
|
||||
Tucker
|
||||
Guzman
|
||||
Burns
|
||||
Crawford
|
||||
Olson
|
||||
Simpson
|
||||
Porter
|
||||
Hunter
|
||||
Gordon
|
||||
Mendez
|
||||
Silva
|
||||
Shaw
|
||||
Snyder
|
||||
Mason
|
||||
Dixon
|
||||
@@ -0,0 +1,104 @@
|
||||
Emma
|
||||
Sophie
|
||||
Julia
|
||||
Mila
|
||||
Tess
|
||||
Sara
|
||||
Anna
|
||||
Noor
|
||||
Lotte
|
||||
Liv
|
||||
Eva
|
||||
Nora
|
||||
Zoë
|
||||
Evi
|
||||
Yara
|
||||
Saar
|
||||
Nina
|
||||
Fenna
|
||||
Lieke
|
||||
Fleur
|
||||
Isa
|
||||
Roos
|
||||
Lynn
|
||||
Sofie
|
||||
Sarah
|
||||
Milou
|
||||
Olivia
|
||||
Maud
|
||||
Lisa
|
||||
Vera
|
||||
Luna
|
||||
Lina
|
||||
Noa
|
||||
Feline
|
||||
Loïs
|
||||
Lena
|
||||
Floor
|
||||
Charlotte
|
||||
Esmee
|
||||
Julie
|
||||
Iris
|
||||
Lara
|
||||
Amber
|
||||
Hailey
|
||||
Mia
|
||||
Lize
|
||||
Isabelle
|
||||
Cato
|
||||
Fenne
|
||||
Sanne
|
||||
Norah
|
||||
Sophia
|
||||
Ella
|
||||
Nova
|
||||
Elin
|
||||
Femke
|
||||
Lizzy
|
||||
Linde
|
||||
Lauren
|
||||
Rosalie
|
||||
Lana
|
||||
Emily
|
||||
Elise
|
||||
Esmée
|
||||
Anne
|
||||
Isabelle
|
||||
Demi
|
||||
Hannah
|
||||
Liva
|
||||
Suze
|
||||
Fay
|
||||
Isabel
|
||||
Benthe
|
||||
Evi
|
||||
Amy
|
||||
Jasmijn
|
||||
Niene
|
||||
Sterre
|
||||
Fenna
|
||||
Fiene
|
||||
Liz
|
||||
Ise
|
||||
Mara
|
||||
Nienke
|
||||
Indy
|
||||
Romy
|
||||
Lola
|
||||
Puck
|
||||
Nora
|
||||
Merel
|
||||
Bente
|
||||
Eline
|
||||
Lily
|
||||
Leah
|
||||
Naomi
|
||||
Mirthe
|
||||
Valerie
|
||||
Noor
|
||||
Liva
|
||||
Jade
|
||||
Juul
|
||||
Lise
|
||||
Myrthe
|
||||
Veerle
|
||||
@@ -0,0 +1,101 @@
|
||||
Daan
|
||||
Luuk
|
||||
Sem
|
||||
Finn
|
||||
Milan
|
||||
Levi
|
||||
Noah
|
||||
Lucas
|
||||
Jesse
|
||||
Thijs
|
||||
Jayden
|
||||
Bram
|
||||
Lars
|
||||
Ruben
|
||||
Thomas
|
||||
Tim
|
||||
Sam
|
||||
Liam
|
||||
Julian
|
||||
Mees
|
||||
Ties
|
||||
Sven
|
||||
Max
|
||||
Gijs
|
||||
David
|
||||
Stijn
|
||||
Jasper
|
||||
Niels
|
||||
Jens
|
||||
Timo
|
||||
Cas
|
||||
Joep
|
||||
Roan
|
||||
Tom
|
||||
Tygo
|
||||
Teun
|
||||
Siem
|
||||
Mats
|
||||
Thijmen
|
||||
Rens
|
||||
Niek
|
||||
Tobias
|
||||
Dex
|
||||
Hugo
|
||||
Robin
|
||||
Nick
|
||||
Floris
|
||||
Pepijn
|
||||
Boaz
|
||||
Olivier
|
||||
Luca
|
||||
Jurre
|
||||
Jelle
|
||||
Guus
|
||||
Koen
|
||||
Bart
|
||||
Olaf
|
||||
Wessel
|
||||
Daniël
|
||||
Job
|
||||
Sander
|
||||
Tijmen
|
||||
Kai
|
||||
Quinten
|
||||
Owen
|
||||
Morris
|
||||
Fedde
|
||||
Joris
|
||||
Jesper
|
||||
Mick
|
||||
Ryan
|
||||
Milo
|
||||
Stan
|
||||
Benjamin
|
||||
Melle
|
||||
Jip
|
||||
Dylan
|
||||
Brent
|
||||
Mick
|
||||
Dean
|
||||
Otis
|
||||
Abel
|
||||
Luc
|
||||
Sepp
|
||||
Vince
|
||||
Rayan
|
||||
Noud
|
||||
Hidde
|
||||
Fabian
|
||||
Jort
|
||||
Damian
|
||||
Boris
|
||||
Sil
|
||||
Moos
|
||||
Aiden
|
||||
Sep
|
||||
Mika
|
||||
Mijs
|
||||
Mika
|
||||
Felix
|
||||
Merlijn
|
||||
@@ -0,0 +1,106 @@
|
||||
de Jong
|
||||
Jansen
|
||||
de Vries
|
||||
van den Berg
|
||||
van Dijk
|
||||
Bakker
|
||||
Janssen
|
||||
Visser
|
||||
Smit
|
||||
Meijer
|
||||
de Boer
|
||||
Mulder
|
||||
de Groot
|
||||
Bos
|
||||
Vos
|
||||
Peters
|
||||
Hendriks
|
||||
van Leeuwen
|
||||
Dekker
|
||||
Brouwer
|
||||
de Wit
|
||||
Dijkstra
|
||||
Smits
|
||||
de Graaf
|
||||
van der Meer
|
||||
van der Linden
|
||||
Kok
|
||||
Jacobs
|
||||
de Haan
|
||||
Vermeulen
|
||||
van den Heuvel
|
||||
van der Veen
|
||||
van den Broek
|
||||
de Bruijn
|
||||
de Bruin
|
||||
van der Heijden
|
||||
Schouten
|
||||
van Beek
|
||||
Willems
|
||||
van Vliet
|
||||
van de Ven
|
||||
Hoekstra
|
||||
Maas
|
||||
Verhoeven
|
||||
Koster
|
||||
van Dam
|
||||
van der Wal
|
||||
Prins
|
||||
Blom
|
||||
Huisman
|
||||
Peeters
|
||||
Kuipers
|
||||
van Veen
|
||||
van Dongen
|
||||
Veenstra
|
||||
Kramer
|
||||
van den Bosch
|
||||
van der Meulen
|
||||
Mol
|
||||
Zwart
|
||||
van der Laan
|
||||
Martens
|
||||
van de Pol
|
||||
Postma
|
||||
Tromp
|
||||
Borst
|
||||
Boon
|
||||
van Doorn
|
||||
Jonker
|
||||
van der Velden
|
||||
Willemsen
|
||||
van Wijk
|
||||
Groen
|
||||
Gerritsen
|
||||
Bosch
|
||||
van Loon
|
||||
van der Ploeg
|
||||
de Ruiter
|
||||
Molenaar
|
||||
Boer
|
||||
Klein
|
||||
de Koning
|
||||
van de Kamp
|
||||
van der Horst
|
||||
Verbeek
|
||||
Vink
|
||||
Goossens
|
||||
Scholten
|
||||
Hartman
|
||||
van Dalen
|
||||
van Elst
|
||||
Brink
|
||||
Boekel
|
||||
van de Berg
|
||||
Berends
|
||||
van der Hoek
|
||||
Kuiper
|
||||
Kooijman
|
||||
de Lange
|
||||
van der Sluis
|
||||
van Gelder
|
||||
Martens
|
||||
van Asselt
|
||||
Timmermans
|
||||
van Vliet
|
||||
van Rijn
|
||||
@@ -0,0 +1,32 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IdentityGeneratorFactory.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 AliasGenerators.Identity.Implementations;
|
||||
|
||||
using AliasGenerators.Identity.Implementations.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Identity generator factory which creates an identity generator based on the language code.
|
||||
/// </summary>
|
||||
public static class IdentityGeneratorFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates an identity generator based on the language code.
|
||||
/// </summary>
|
||||
/// <param name="languageCode">Two letter language code.</param>
|
||||
/// <returns>The IdentityGenerator for the requested language.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown if no identity generator is found for the requested language.</exception>
|
||||
public static IdentityGenerator CreateIdentityGenerator(string languageCode)
|
||||
{
|
||||
return languageCode.ToLower() switch
|
||||
{
|
||||
"nl" => new IdentityGeneratorNl(),
|
||||
"en" => new IdentityGeneratorEn(),
|
||||
_ => throw new ArgumentException($"Unsupported language code: {languageCode}", nameof(languageCode)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="FigIdentityGenerator.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 AliasGenerators.Identity.Implementations;
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Identity generator which generates random identities using the identiteitgenerator.nl semi-public API.
|
||||
/// </summary>
|
||||
public class FigIdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
private static readonly string Url = "https://api.identiteitgenerator.nl/generate/identity";
|
||||
private static readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
|
||||
{
|
||||
var response = await HttpClient.GetAsync(Url);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, JsonSerializerOptions);
|
||||
|
||||
if (identity is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize the identity from FIG WebApi.");
|
||||
}
|
||||
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IdentityGeneratorEn.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 AliasGenerators.Identity.Implementations;
|
||||
|
||||
using AliasGenerators.Identity.Implementations.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Dutch identity generator which implements IIdentityGenerator and generates
|
||||
/// random dutch identities.
|
||||
/// </summary>
|
||||
public class IdentityGeneratorEn : IdentityGenerator
|
||||
{
|
||||
/// <inheritdoc cref="IdentityGenerator.FirstNamesListMale" />
|
||||
protected override string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Dictionaries.en.firstnames_male";
|
||||
|
||||
/// <inheritdoc cref="IdentityGenerator.FirstNamesListFemale" />
|
||||
protected override string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Dictionaries.en.firstnames_female";
|
||||
|
||||
/// <inheritdoc cref="IdentityGenerator.LastNamesList" />
|
||||
protected override string LastNamesList => "AliasGenerators.Identity.Implementations.Dictionaries.en.lastnames";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IdentityGeneratorNl.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 AliasGenerators.Identity.Implementations;
|
||||
|
||||
using AliasGenerators.Identity.Implementations.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Dutch identity generator which implements IIdentityGenerator and generates
|
||||
/// random dutch identities.
|
||||
/// </summary>
|
||||
public class IdentityGeneratorNl : IdentityGenerator
|
||||
{
|
||||
/// <inheritdoc cref="IdentityGenerator.FirstNamesListMale" />
|
||||
protected override string FirstNamesListMale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_male";
|
||||
|
||||
/// <inheritdoc cref="IdentityGenerator.FirstNamesListFemale" />
|
||||
protected override string FirstNamesListFemale => "AliasGenerators.Identity.Implementations.Dictionaries.nl.firstnames_female";
|
||||
|
||||
/// <inheritdoc cref="IdentityGenerator.LastNamesList" />
|
||||
protected override string LastNamesList => "AliasGenerators.Identity.Implementations.Dictionaries.nl.lastnames";
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="StaticIdentityGenerator.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 AliasGenerators.Identity.Implementations;
|
||||
|
||||
using AliasGenerators.Identity;
|
||||
|
||||
/// <summary>
|
||||
/// Static identity generator which implements IIdentityGenerator but always returns
|
||||
/// the same static identity for testing purposes.
|
||||
/// </summary>
|
||||
public class StaticIdentityGenerator : IIdentityGenerator
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public async Task<Identity.Models.Identity> GenerateRandomIdentityAsync()
|
||||
{
|
||||
await Task.Yield(); // Add an await statement to make the method truly asynchronous.
|
||||
return new Identity.Models.Identity
|
||||
{
|
||||
FirstName = "John",
|
||||
LastName = "Doe",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Address.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 AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Address model.
|
||||
/// </summary>
|
||||
public class Address
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the street.
|
||||
/// </summary>
|
||||
public string Street { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
public string City { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
public string State { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the zip code.
|
||||
/// </summary>
|
||||
public string ZipCode { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the country.
|
||||
/// </summary>
|
||||
public string Country { get; set; } = null!;
|
||||
}
|
||||
24
src/AliasGenerators/Identity/Models/Gender.cs
Normal file
24
src/AliasGenerators/Identity/Models/Gender.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Gender.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 AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Identity model.
|
||||
/// </summary>
|
||||
public enum Gender
|
||||
{
|
||||
/// <summary>
|
||||
/// Male gender.
|
||||
/// </summary>
|
||||
Male,
|
||||
|
||||
/// <summary>
|
||||
/// Female gender.
|
||||
/// </summary>
|
||||
Female,
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
@@ -19,7 +20,7 @@ public class Identity
|
||||
/// <summary>
|
||||
/// Gets or sets the gender.
|
||||
/// </summary>
|
||||
public int Gender { get; set; }
|
||||
public Gender Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the first name.
|
||||
@@ -41,48 +42,8 @@ public class Identity
|
||||
/// </summary>
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address.
|
||||
/// </summary>
|
||||
public Address Address { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the job.
|
||||
/// </summary>
|
||||
public Job Job { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hobbies.
|
||||
/// </summary>
|
||||
public List<string> Hobbies { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email address prefix.
|
||||
/// </summary>
|
||||
public string EmailPrefix { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the phone mobile.
|
||||
/// </summary>
|
||||
public string PhoneMobile { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bank account IBAN.
|
||||
/// </summary>
|
||||
public string BankAccountIBAN { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile photo in base64 format.
|
||||
/// </summary>
|
||||
public string ProfilePhotoBase64 { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile photo prompt.
|
||||
/// </summary>
|
||||
public string ProfilePhotoPrompt { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Job.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 AliasGenerators.Identity.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Job model.
|
||||
/// </summary>
|
||||
public class Job
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the title.
|
||||
/// </summary>
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the company.
|
||||
/// </summary>
|
||||
public string Company { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the salary.
|
||||
/// </summary>
|
||||
public string Salary { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the calculated salary.
|
||||
/// </summary>
|
||||
public decimal SalaryCalculated { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description.
|
||||
/// </summary>
|
||||
public string Description { get; set; } = null!;
|
||||
}
|
||||
143
src/AliasGenerators/Identity/UsernameEmailGenerator.cs
Normal file
143
src/AliasGenerators/Identity/UsernameEmailGenerator.cs
Normal file
@@ -0,0 +1,143 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UsernameEmailGenerator.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 AliasGenerators.Identity;
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
/// <summary>
|
||||
/// Generates usernames and email prefixes based on an identity.
|
||||
/// </summary>
|
||||
public class UsernameEmailGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Minimum length of the generated username.
|
||||
/// </summary>
|
||||
private const int MinLength = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum length of the generated username.
|
||||
/// </summary>
|
||||
private const int MaxLength = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new random instance for generating random values.
|
||||
/// </summary>
|
||||
private readonly Random _random = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of allowed symbols to use in usernames.
|
||||
/// </summary>
|
||||
private readonly List<string> _symbols = [".", "-"];
|
||||
|
||||
/// <summary>
|
||||
/// Generates a username based on a identity.
|
||||
/// </summary>
|
||||
/// <param name="identity">Identity to generate username for.</param>
|
||||
/// <returns>Username as string.</returns>
|
||||
public string GenerateUsername(Models.Identity identity)
|
||||
{
|
||||
// Generate username based on email prefix but strip all non-alphanumeric characters
|
||||
string username = GenerateEmailPrefix(identity);
|
||||
username = Regex.Replace(username, @"[^a-zA-Z0-9]", string.Empty, RegexOptions.NonBacktracking);
|
||||
|
||||
// Adjust length
|
||||
if (username.Length < MinLength)
|
||||
{
|
||||
username += GenerateRandomString(MinLength - username.Length);
|
||||
}
|
||||
else if (username.Length > MaxLength)
|
||||
{
|
||||
username = username.Substring(0, MaxLength);
|
||||
}
|
||||
|
||||
return username;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a valid email prefix based on an identity.
|
||||
/// </summary>
|
||||
/// <param name="identity">Identity to generate email prefix for.</param>
|
||||
/// <returns>Valid email prefix as string.</returns>
|
||||
public string GenerateEmailPrefix(Models.Identity identity)
|
||||
{
|
||||
var parts = new List<string>();
|
||||
|
||||
// Use first initial + last name
|
||||
if (_random.Next(2) == 0)
|
||||
{
|
||||
parts.Add(identity.FirstName.Substring(0, 1).ToLower() + identity.LastName.ToLower());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use full name
|
||||
parts.Add((identity.FirstName + identity.LastName).ToLower());
|
||||
}
|
||||
|
||||
// Add birth year
|
||||
if (_random.Next(2) == 0)
|
||||
{
|
||||
parts.Add(identity.BirthDate.Year.ToString().Substring(2));
|
||||
}
|
||||
|
||||
// Join parts and sanitize
|
||||
var emailPrefix = string.Join(GetRandomSymbol(), parts);
|
||||
emailPrefix = SanitizeEmailPrefix(emailPrefix);
|
||||
|
||||
// Adjust length
|
||||
if (emailPrefix.Length < MinLength)
|
||||
{
|
||||
emailPrefix += GenerateRandomString(MinLength - emailPrefix.Length);
|
||||
}
|
||||
else if (emailPrefix.Length > MaxLength)
|
||||
{
|
||||
emailPrefix = emailPrefix.Substring(0, MaxLength);
|
||||
}
|
||||
|
||||
return emailPrefix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize the email prefix by removing invalid characters and ensuring it's a valid email prefix.
|
||||
/// </summary>
|
||||
/// <param name="input">The input string to sanitize.</param>
|
||||
/// <returns>The sanitized string.</returns>
|
||||
private static string SanitizeEmailPrefix(string input)
|
||||
{
|
||||
// Remove any character that's not a letter, number, dot, underscore, or hyphen including special characters
|
||||
string sanitized = Regex.Replace(input, @"[^a-zA-Z0-9._-]", string.Empty, RegexOptions.NonBacktracking);
|
||||
|
||||
// Remove consecutive dots, underscores, or hyphens
|
||||
sanitized = Regex.Replace(sanitized, @"[-_.]{2,}", m => m.Value[0].ToString(), RegexOptions.NonBacktracking);
|
||||
|
||||
// Ensure it doesn't start or end with a dot, underscore, or hyphen
|
||||
sanitized = sanitized.Trim('.', '_', '-');
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a random symbol from the list of symbols.
|
||||
/// </summary>
|
||||
/// <returns>Random symbol.</returns>
|
||||
private string GetRandomSymbol()
|
||||
{
|
||||
return _random.Next(3) == 0 ? _symbols[_random.Next(_symbols.Count)] : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of a given length.
|
||||
/// </summary>
|
||||
/// <param name="length">Length of string to generate.</param>
|
||||
/// <returns>String with random characters.</returns>
|
||||
private string GenerateRandomString(int length)
|
||||
{
|
||||
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
return new string(Enumerable.Repeat(chars, length)
|
||||
.Select(s => s[_random.Next(s.Length)]).ToArray());
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,8 @@
|
||||
<ProjectReference Include="..\Utilities\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Main\Components\Refresh\RefreshButton.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
@implements IDisposable
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
@foreach (var message in Messages)
|
||||
@if (Messages.Count == 0)
|
||||
{
|
||||
if (message.Key == "success")
|
||||
{
|
||||
<AlertMessageSuccess Message="@message.Value" />
|
||||
}
|
||||
return;
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "error")
|
||||
|
||||
<div class="messages-container grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
<AlertMessageError Message="@message.Value" />
|
||||
if (message.Key == "success")
|
||||
{
|
||||
<AlertMessageSuccess Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "error")
|
||||
{
|
||||
<AlertMessageError Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.messages-container > :last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private List<KeyValuePair<string, string>> Messages { get; set; } = new();
|
||||
private bool _onChangeSubscribed = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
@@ -26,22 +39,26 @@
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
// We subscribe to the OnChange event of the PortalMessageService to update the UI when a new message is added
|
||||
RefreshAddMessages();
|
||||
GlobalNotificationService.OnChange += RefreshAddMessages;
|
||||
_onChangeSubscribed = true;
|
||||
NavigationManager.LocationChanged += HandleLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// We unsubscribe from the OnChange event of the PortalMessageService when the component is disposed
|
||||
if (_onChangeSubscribed)
|
||||
{
|
||||
GlobalNotificationService.OnChange -= RefreshAddMessages;
|
||||
_onChangeSubscribed = false;
|
||||
}
|
||||
GlobalNotificationService.OnChange -= RefreshAddMessages;
|
||||
NavigationManager.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the messages on navigation to another page.
|
||||
/// </summary>
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
RefreshAddMessages();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
<GlobalNotificationDisplay />
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<div class="flex pt-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
||||
<main>
|
||||
<GlobalNotificationDisplay />
|
||||
@Body
|
||||
</main>
|
||||
<Footer></Footer>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
Logger.LogInformation("User changed their password successfully.");
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Your password has been changed.", true);
|
||||
GlobalNotificationService.AddSuccessMessage("Your password has been changed.");
|
||||
|
||||
NavigationService.RedirectToCurrentPage();
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
var userId = await UserManager.GetUserIdAsync(UserService.User());
|
||||
Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
|
||||
|
||||
GlobalNotificationService.AddSuccessMessage("Your authenticator app key has been reset, you will need to re-configure your authenticator app using the new key.", true);
|
||||
GlobalNotificationService.AddSuccessMessage("Your authenticator app key has been reset, you will need to re-configure your authenticator app using the new key.");
|
||||
|
||||
NavigationService.RedirectTo(
|
||||
"account/manage/2fa");
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
|
||||
<div class="mb-4 col-span-full xl:mb-2">
|
||||
<GlobalNotificationDisplay />
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Manage account</h1>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
@using AliasVault.Admin.Main.Components.Layout
|
||||
@using AliasVault.Admin.Main.Components.Loading
|
||||
@using AliasVault.Admin.Main.Components.WorkerStatus
|
||||
@using AliasVault.Admin.Main.Components.Refresh
|
||||
@using AliasVault.RazorComponents
|
||||
@using AliasVault.Admin.Main.Models
|
||||
@using AliasVault.Admin.Main.Pages
|
||||
@using AliasVault.Admin.Services
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "aliasvault.client",
|
||||
"name": "aliasvault.admin",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -644,14 +644,6 @@ video {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.col-span-6 {
|
||||
grid-column: span 6 / span 6;
|
||||
}
|
||||
|
||||
.mx-3 {
|
||||
margin-left: 0.75rem;
|
||||
margin-right: 0.75rem;
|
||||
@@ -667,6 +659,10 @@ video {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
@@ -743,10 +739,6 @@ video {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.line-clamp-1 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
@@ -822,10 +814,18 @@ video {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-10 {
|
||||
width: 2.5rem;
|
||||
}
|
||||
|
||||
.w-2\/3 {
|
||||
width: 66.666667%;
|
||||
}
|
||||
|
||||
.w-4 {
|
||||
width: 1rem;
|
||||
}
|
||||
@@ -858,14 +858,6 @@ video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-1\/3 {
|
||||
width: 33.333333%;
|
||||
}
|
||||
|
||||
.w-2\/3 {
|
||||
width: 66.666667%;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
@@ -882,10 +874,6 @@ video {
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
.flex-1 {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
.flex-shrink-0 {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -924,6 +912,10 @@ video {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
@@ -932,14 +924,6 @@ video {
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-6 {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -972,10 +956,6 @@ video {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.25rem * var(--tw-space-x-reverse));
|
||||
@@ -1140,11 +1120,6 @@ video {
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-blue-600 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -1344,6 +1319,14 @@ video {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.ps-2 {
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
@@ -1360,14 +1343,6 @@ video {
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.pl-2 {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.pr-2 {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -1518,11 +1493,6 @@ video {
|
||||
color: rgb(133 77 14 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-blue-500 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(59 130 246 / var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.underline {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
@@ -1595,11 +1565,6 @@ video {
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.hover\:bg-blue-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-gray-100:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
|
||||
@@ -1625,16 +1590,16 @@ video {
|
||||
background-color: rgb(154 93 38 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-red-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-red-700:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:bg-red-800:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.hover\:text-gray-900:hover {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(17 24 39 / var(--tw-text-opacity));
|
||||
@@ -1750,11 +1715,6 @@ video {
|
||||
border-color: rgb(55 65 81 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-blue-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-gray-600:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -1785,6 +1745,11 @@ video {
|
||||
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-900:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
|
||||
@@ -1795,11 +1760,6 @@ video {
|
||||
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-red-500:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:bg-opacity-80:is(.dark *) {
|
||||
--tw-bg-opacity: 0.8;
|
||||
}
|
||||
@@ -1893,11 +1853,6 @@ video {
|
||||
--tw-ring-offset-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-blue-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.dark\:hover\:bg-gray-600:hover:is(.dark *) {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
|
||||
@@ -1948,11 +1903,6 @@ video {
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-blue-800:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(30 64 175 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-gray-600:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
|
||||
@@ -1978,21 +1928,17 @@ video {
|
||||
--tw-ring-color: rgb(154 93 38 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-red-900:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.dark\:focus\:ring-red-800:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(153 27 27 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:col-span-3 {
|
||||
grid-column: span 3 / span 3;
|
||||
}
|
||||
.dark\:focus\:ring-red-900:focus:is(.dark *) {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(127 29 29 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.sm\:flex {
|
||||
display: flex;
|
||||
}
|
||||
@@ -2011,18 +1957,18 @@ video {
|
||||
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.sm\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.sm\:space-y-0 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.sm\:p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
@@ -2116,10 +2062,6 @@ video {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.lg\:col-auto {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.lg\:mb-10 {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
@@ -2180,17 +2122,17 @@ video {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.xl\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(2rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.xl\:space-x-0 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0px * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.xl\:space-x-8 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(2rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
@@ -2198,10 +2140,6 @@ video {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.\32xl\:flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.\32xl\:space-x-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(1rem * var(--tw-space-x-reverse));
|
||||
|
||||
@@ -11,6 +11,7 @@ using AliasServerDb;
|
||||
using AliasVault.Api.Helpers;
|
||||
using AliasVault.Shared.Models.Spamok;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using AliasVault.Shared.Models.WebApi.Email;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -25,7 +26,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the newest version of the vault for the current user.
|
||||
/// Returns a list of emails for the provided email address.
|
||||
/// </summary>
|
||||
/// <param name="to">The full email address including @ sign.</param>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
@@ -98,4 +99,72 @@ public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContex
|
||||
|
||||
return Ok(returnValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of emails for the provided email address.
|
||||
/// </summary>
|
||||
/// <param name="model">The request model extracted from POST body.</param>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
[HttpPost(template: "bulk", Name = "GetEmailBoxBulk")]
|
||||
public async Task<IActionResult> GetEmailBoxBulk([FromBody] MailboxBulkRequest model)
|
||||
{
|
||||
await using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user is null)
|
||||
{
|
||||
return Unauthorized("Not authenticated.");
|
||||
}
|
||||
|
||||
// Sanitize input.
|
||||
model.Addresses = model.Addresses.Select(x => x.Trim().ToLower()).ToList();
|
||||
model.PageSize = Math.Min(model.PageSize, 50);
|
||||
|
||||
// Load all email addresses that the user has a claim to where the address is in the list.
|
||||
var emailClaims = await context.UserEmailClaims
|
||||
.Where(claim => claim.UserId == user.Id && model.Addresses.Contains(claim.Address))
|
||||
.ToListAsync();
|
||||
|
||||
var query = context.Emails
|
||||
.AsNoTracking()
|
||||
.Include(x => x.EncryptionKey)
|
||||
.Where(email => context.UserEmailClaims
|
||||
.Any(claim => claim.UserId == user.Id
|
||||
&& claim.Address == email.To
|
||||
&& model.Addresses.Contains(claim.Address)));
|
||||
|
||||
var totalRecords = await query.CountAsync();
|
||||
|
||||
List<MailboxEmailApiModel> emails = await query.Select(x => new MailboxEmailApiModel
|
||||
{
|
||||
Id = x.Id,
|
||||
Subject = x.Subject,
|
||||
FromDisplay = ConversionHelper.ConvertFromToFromDisplay(x.From),
|
||||
FromDomain = x.FromDomain,
|
||||
FromLocal = x.FromLocal,
|
||||
ToDomain = x.ToDomain,
|
||||
ToLocal = x.ToLocal,
|
||||
Date = x.Date,
|
||||
DateSystem = x.DateSystem,
|
||||
SecondsAgo = (int)DateTime.UtcNow.Subtract(x.DateSystem).TotalSeconds,
|
||||
MessagePreview = x.MessagePreview ?? string.Empty,
|
||||
EncryptedSymmetricKey = x.EncryptedSymmetricKey,
|
||||
EncryptionKey = x.EncryptionKey.PublicKey,
|
||||
})
|
||||
.OrderByDescending(x => x.DateSystem)
|
||||
.Skip((model.Page - 1) * model.PageSize)
|
||||
.Take(model.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
MailboxBulkResponse returnValue = new()
|
||||
{
|
||||
Addresses = emailClaims.Select(x => x.Address).ToList(),
|
||||
Mails = emails,
|
||||
PageSize = model.PageSize,
|
||||
CurrentPage = model.Page,
|
||||
TotalRecords = totalRecords,
|
||||
};
|
||||
|
||||
return Ok(returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
public class IdentityController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxies the request to the identity generator to generate a random identity.
|
||||
/// Proxies the request to the english identity generator to generate a random identity.
|
||||
/// </summary>
|
||||
/// <returns>Identity model.</returns>
|
||||
[HttpGet("Generate")]
|
||||
@@ -33,7 +33,7 @@ public class IdentityController(UserManager<AliasVaultUser> userManager) : Authe
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var identityGenerator = new FigIdentityGenerator();
|
||||
var identityGenerator = new IdentityGeneratorEn();
|
||||
return Ok(await identityGenerator.GenerateRandomIdentityAsync());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,33 +151,36 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
|
||||
// Register new email addresses.
|
||||
foreach (var email in newEmailAddresses)
|
||||
{
|
||||
// Sanitize email address.
|
||||
var sanitizedEmail = email.Trim().ToLower();
|
||||
|
||||
// If email address is invalid according to the EmailAddressAttribute, skip it.
|
||||
if (!new EmailAddressAttribute().IsValid(email))
|
||||
if (!new EmailAddressAttribute().IsValid(sanitizedEmail))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if the email address is already claimed (by another user).
|
||||
var existingClaim = await context.UserEmailClaims
|
||||
.FirstOrDefaultAsync(x => x.Address == email);
|
||||
.FirstOrDefaultAsync(x => x.Address == sanitizedEmail);
|
||||
|
||||
if (existingClaim != null && existingClaim.UserId != userId)
|
||||
{
|
||||
// Email address is already claimed by another user. Log the error and continue.
|
||||
logger.LogWarning("{User} tried to claim email address: {Email} but it is already claimed by another user.", userId, email);
|
||||
logger.LogWarning("{User} tried to claim email address: {Email} but it is already claimed by another user.", userId, sanitizedEmail);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!existingEmailClaims.Contains(email))
|
||||
if (!existingEmailClaims.Contains(sanitizedEmail))
|
||||
{
|
||||
try
|
||||
{
|
||||
await context.UserEmailClaims.AddAsync(new UserEmailClaim
|
||||
{
|
||||
UserId = userId,
|
||||
Address = email,
|
||||
AddressLocal = email.Split('@')[0],
|
||||
AddressDomain = email.Split('@')[1],
|
||||
Address = sanitizedEmail,
|
||||
AddressLocal = sanitizedEmail.Split('@')[0],
|
||||
AddressDomain = sanitizedEmail.Split('@')[1],
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
});
|
||||
@@ -185,7 +188,7 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
|
||||
catch (DbUpdateException ex)
|
||||
{
|
||||
// Error while adding email claim. Log the error and continue.
|
||||
logger.LogWarning(ex, "Error while adding UserEmailClaim with email: {Email} for user: {UserId}.", email, userId);
|
||||
logger.LogWarning(ex, "Error while adding UserEmailClaim with email: {Email} for user: {UserId}.", sanitizedEmail, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<ProjectReference Include="..\Databases\AliasClientDb\AliasClientDb.csproj" />
|
||||
<ProjectReference Include="..\AliasGenerators\AliasGenerators.csproj" />
|
||||
<ProjectReference Include="..\AliasVault.Shared\AliasVault.Shared.csproj" />
|
||||
<ProjectReference Include="..\Utilities\AliasVault.RazorComponents\AliasVault.RazorComponents.csproj" />
|
||||
<ProjectReference Include="..\Utilities\Cryptography\Cryptography.csproj" />
|
||||
<ProjectReference Include="..\Utilities\CsvImportExport\CsvImportExport.csproj" />
|
||||
<ProjectReference Include="..\Utilities\FaviconExtractor\FaviconExtractor.csproj" />
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject NavigationManager NavigationManager
|
||||
@implements IDisposable
|
||||
|
||||
@foreach (var message in Messages)
|
||||
@if (Messages.Count == 0)
|
||||
{
|
||||
if (message.Key == "success")
|
||||
{
|
||||
<AlertMessageSuccess Message="@message.Value" />
|
||||
}
|
||||
return;
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "error")
|
||||
|
||||
<div class="messages-container grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
<AlertMessageError Message="@message.Value" />
|
||||
if (message.Key == "success")
|
||||
{
|
||||
<AlertMessageSuccess Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
}
|
||||
@foreach (var message in Messages)
|
||||
{
|
||||
if (message.Key == "error")
|
||||
{
|
||||
<AlertMessageError Message="@message.Value" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.messages-container > :last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@code {
|
||||
private List<KeyValuePair<string, string>> Messages { get; set; } = new();
|
||||
private bool _onChangeSubscribed = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
@@ -27,22 +40,26 @@
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
// We subscribe to the OnChange event of the PortalMessageService to update the UI when a new message is added
|
||||
RefreshAddMessages();
|
||||
GlobalNotificationService.OnChange += RefreshAddMessages;
|
||||
_onChangeSubscribed = true;
|
||||
NavigationManager.LocationChanged += HandleLocationChanged;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
// We unsubscribe from the OnChange event of the PortalMessageService when the component is disposed
|
||||
if (_onChangeSubscribed)
|
||||
{
|
||||
GlobalNotificationService.OnChange -= RefreshAddMessages;
|
||||
_onChangeSubscribed = false;
|
||||
}
|
||||
GlobalNotificationService.OnChange -= RefreshAddMessages;
|
||||
NavigationManager.LocationChanged -= HandleLocationChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the messages on navigation to another page.
|
||||
/// </summary>
|
||||
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
RefreshAddMessages();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">From: @Email?.FromDisplay</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">To: @(Email?.ToLocal)@@@(Email?.ToDomain)</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">Date: @Email?.DateSystem</p>
|
||||
</div>
|
||||
<div class="mt-4 text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
@inject JsInteropService JsInteropService
|
||||
@inject DbService DbService
|
||||
@inject Config Config
|
||||
@inject EmailService EmailService
|
||||
@using System.Timers
|
||||
@implements IDisposable
|
||||
|
||||
@@ -90,7 +91,7 @@
|
||||
/// The email address to show recent emails for.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string EmailAddress { get; set; } = string.Empty;
|
||||
public string? EmailAddress { get; set; } = string.Empty;
|
||||
|
||||
private List<MailboxEmailApiModel> MailboxEmails { get; set; } = new();
|
||||
private bool ShowComponent { get; set; } = false;
|
||||
@@ -110,6 +111,11 @@
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
if (EmailAddress is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if email has a known SpamOK domain, if not, don't show this component.
|
||||
if (IsSpamOkDomain(EmailAddress) || IsAliasVaultDomain(EmailAddress))
|
||||
{
|
||||
@@ -130,6 +136,12 @@
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
if (EmailAddress is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsSpamOk = IsSpamOkDomain(EmailAddress);
|
||||
}
|
||||
|
||||
@@ -191,7 +203,7 @@
|
||||
|
||||
private async Task LoadRecentEmailsAsync()
|
||||
{
|
||||
if (!ShowComponent)
|
||||
if (!ShowComponent || EmailAddress is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -217,6 +229,11 @@
|
||||
/// </summary>
|
||||
private async Task OpenEmail(int emailId)
|
||||
{
|
||||
if (EmailAddress is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get email prefix, which is the part before the @ symbol.
|
||||
string emailPrefix = EmailAddress.Split('@')[0];
|
||||
|
||||
@@ -292,7 +309,7 @@
|
||||
break;
|
||||
case "CLAIM_DOES_NOT_EXIST":
|
||||
Error = "An error occurred while trying to load the emails. Please try to edit and " +
|
||||
"save the credential entry to synchronize the database, then again.";
|
||||
"save the credential entry to synchronize the database, then try again.";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(errorResponse.Message);
|
||||
@@ -325,24 +342,7 @@
|
||||
MailboxEmails = mailbox.Mails.Take(10).ToList();
|
||||
}
|
||||
|
||||
// Loop through emails and decrypt the subject locally.
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var privateKeys = await context.EncryptionKeys.ToListAsync();
|
||||
foreach (var mail in MailboxEmails)
|
||||
{
|
||||
var privateKey = privateKeys.First(x => x.PublicKey == mail.EncryptionKey);
|
||||
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
MailboxEmails = await EmailService.DecryptEmailList(MailboxEmails);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -358,28 +358,7 @@
|
||||
var privateKey = await context.EncryptionKeys.FirstOrDefaultAsync(x => x.PublicKey == mail.EncryptionKey);
|
||||
if (privateKey is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await JsInteropService.DecryptWithPrivateKey(mail.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
mail.Subject = await JsInteropService.SymmetricDecrypt(mail.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
if (mail.MessageHtml is not null)
|
||||
{
|
||||
mail.MessageHtml = await JsInteropService.SymmetricDecrypt(mail.MessageHtml, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
|
||||
if (mail.MessagePlain is not null)
|
||||
{
|
||||
mail.MessagePlain = await JsInteropService.SymmetricDecrypt(mail.MessagePlain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
|
||||
mail.FromDisplay = await JsInteropService.SymmetricDecrypt(mail.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
mail.FromLocal = await JsInteropService.SymmetricDecrypt(mail.FromLocal, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
mail.FromDomain = await JsInteropService.SymmetricDecrypt(mail.FromDomain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error = ex.Message;
|
||||
}
|
||||
mail = await EmailService.DecryptEmail(mail);
|
||||
}
|
||||
|
||||
Email = mail;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
@using System.Text.RegularExpressions
|
||||
|
||||
<div title="@SenderEmail" class="justify-content-center align-middle" style="padding-top:10px;font-size:18px;color:white;text-align:center;border-radius:30px;width: 50px;height: 50px;background-color:@SenderNameColor;">@SenderNameLetters</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The name of the sender.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SenderName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The email of the sender.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SenderEmail { get; set; } = string.Empty;
|
||||
|
||||
private string SenderNameLetters { get; set; } = string.Empty;
|
||||
private string SenderNameColor { get; set; } = "#666";
|
||||
|
||||
/// <summary>
|
||||
/// Mappping of alphabet letters to HSL colors.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> AlphabetColors = new Dictionary<string, string>
|
||||
{
|
||||
{ "A", "hsl(175, 50%, 50%)" },
|
||||
{ "B", "hsl(234, 50%, 50%)" },
|
||||
{ "C", "hsl(278, 50%, 50%)" },
|
||||
{ "D", "hsl(191, 50%, 50%)" },
|
||||
{ "E", "hsl(215, 50%, 50%)" },
|
||||
{ "F", "hsl(315, 50%, 50%)" },
|
||||
{ "G", "hsl(247, 50%, 50%)" },
|
||||
{ "H", "hsl(259, 50%, 50%)" },
|
||||
{ "I", "hsl(289, 50%, 50%)" },
|
||||
{ "J", "hsl(206, 50%, 50%)" },
|
||||
{ "K", "hsl(124, 50%, 50%)" },
|
||||
{ "L", "hsl(129, 50%, 50%)" },
|
||||
{ "M", "hsl(69, 50%, 50%)" },
|
||||
{ "N", "hsl(38, 50%, 50%)" },
|
||||
{ "O", "hsl(352, 50%, 50%)" },
|
||||
{ "P", "hsl(311, 50%, 50%)" },
|
||||
{ "Q", "hsl(332, 50%, 50%)" },
|
||||
{ "R", "hsl(344, 50%, 50%)" },
|
||||
{ "S", "hsl(357, 50%, 50%)" },
|
||||
{ "T", "hsl(23, 50%, 50%)" },
|
||||
{ "U", "hsl(16, 50%, 50%)" },
|
||||
{ "V", "hsl(304, 50%, 50%)" },
|
||||
{ "W", "hsl(300, 50%, 50%)" },
|
||||
{ "X", "hsl(332, 50%, 50%)" },
|
||||
{ "Y", "hsl(48, 50%, 50%)" },
|
||||
{ "Z", "hsl(9, 50%, 50%)" }
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
GenerateSenderInitials();
|
||||
GenerateSenderColor();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract the initials of the sender.
|
||||
/// </summary>
|
||||
private void GenerateSenderInitials()
|
||||
{
|
||||
string senderName = Regex.Replace(SenderName, "[^a-zA-Z ]", "", RegexOptions.NonBacktracking);
|
||||
|
||||
SenderNameLetters = string.Concat(senderName
|
||||
.Split(' ')
|
||||
.Select(n => n.Length > 0 ? n[0].ToString() : "")
|
||||
);
|
||||
SenderNameLetters = SenderNameLetters.Substring(0, Math.Min(2, SenderNameLetters.Length)).ToUpper();
|
||||
|
||||
if (string.IsNullOrEmpty(SenderNameLetters))
|
||||
{
|
||||
SenderNameLetters = "?";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pick a unique color based on the first letter of the sender name.
|
||||
/// </summary>
|
||||
private void GenerateSenderColor()
|
||||
{
|
||||
string firstLetter = SenderNameLetters.Substring(0, 1);
|
||||
SenderNameColor = AlphabetColors.TryGetValue(firstLetter, out var color) ? color : "#666";
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@
|
||||
/// The value of the input field. This should be the full email address.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string? Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
@@ -113,6 +113,11 @@
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
if (Value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Value.Contains('@'))
|
||||
{
|
||||
SelectedDomain = Value.Split('@')[1];
|
||||
@@ -176,6 +181,11 @@
|
||||
|
||||
private void ToggleCustomDomain()
|
||||
{
|
||||
if (Value is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsCustomDomain = !IsCustomDomain;
|
||||
if (!IsCustomDomain && !Value.Contains('@'))
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" id="@Id" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
<input type="text" id="@Id" @onfocus="OnFocusEvent" class="outline-0 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg block w-full p-2.5 pr-10 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" value="@Value" @oninput="OnInputChanged">
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -35,6 +35,12 @@
|
||||
[Parameter]
|
||||
public string Value { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public EventCallback<FocusEventArgs> OnFocus { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Callback that is triggered when the value changes.
|
||||
/// </summary>
|
||||
@@ -46,4 +52,12 @@
|
||||
Value = e.Value?.ToString() ?? string.Empty;
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private async Task OnFocusEvent(FocusEventArgs e)
|
||||
{
|
||||
if (OnFocus.HasDelegate)
|
||||
{
|
||||
await OnFocus.InvokeAsync(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
<svg class="w-6 h-6 text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"></path></svg>
|
||||
<a href="@item.Url" class="ml-1 text-gray-700 hover:text-primary-600 md:ml-2 dark:text-gray-300 dark:hover:text-primary-500">@item.DisplayName</a>
|
||||
<a href="@item.Url" class="text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-500">@item.DisplayName</a>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@@ -31,7 +31,6 @@
|
||||
}
|
||||
</ol>
|
||||
</nav>
|
||||
<GlobalNotificationDisplay />
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Module != null)
|
||||
if (Module is not null)
|
||||
{
|
||||
await Module.InvokeVoidAsync("unregisterClickOutsideHandler");
|
||||
await Module.DisposeAsync();
|
||||
@@ -69,9 +69,9 @@
|
||||
/// </summary>
|
||||
private async Task LoadModuleAsync()
|
||||
{
|
||||
if (Module == null)
|
||||
if (Module is null)
|
||||
{
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/clickOutsideHandler.js");
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/clickOutsideHandler.js");
|
||||
ObjRef = DotNetObjectReference.Create(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
@using System.ComponentModel.DataAnnotations
|
||||
@inherits AliasVault.Client.Main.Pages.MainBase
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject CredentialService CredentialService
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<button @ref="buttonRef" @onclick="TogglePopup" id="quickIdentityButton" class="px-4 ms-5 py-2 text-sm font-medium text-white bg-gradient-to-r from-primary-500 to-primary-600 hover:from-primary-600 hover:to-primary-700 focus:outline-none dark:from-primary-400 dark:to-primary-500 dark:hover:from-primary-500 dark:hover:to-primary-600 rounded-md shadow-sm transition duration-150 ease-in-out transform hover:scale-105 active:scale-95 focus:shadow-outline">
|
||||
+ <span class="hidden md:inline">New identity</span>
|
||||
</button>
|
||||
|
||||
@if (IsPopupVisible)
|
||||
{
|
||||
<ClickOutsideHandler OnClose="ClosePopup" ContentId="quickIdentityPopup,quickIdentityButton">
|
||||
<div id="quickIdentityPopup" class="absolute z-50 mt-2 p-4 bg-white rounded-lg shadow-xl border border-gray-300"
|
||||
style="@PopupStyle">
|
||||
<h3 class="text-lg font-semibold mb-4">Create New Identity</h3>
|
||||
<EditForm Model="Model" OnValidSubmit="CreateIdentity">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="mb-4">
|
||||
<EditFormRow Id="serviceName" Label="Service Name" @bind-Value="Model.ServiceName"></EditFormRow>
|
||||
<ValidationMessage For="() => Model.ServiceName"/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<EditFormRow Id="serviceUrl" Label="Service URL" OnFocus="OnFocusUrlInput" @bind-Value="Model.ServiceUrl"></EditFormRow>
|
||||
<ValidationMessage For="() => Model.ServiceUrl"/>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<button id="quickIdentitySubmit" type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<a href="#" @onclick="OpenAdvancedMode" @onclick:preventDefault class="text-sm text-blue-500 hover:text-blue-700">
|
||||
Create via advanced mode
|
||||
</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
</div>
|
||||
</ClickOutsideHandler>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool IsPopupVisible = false;
|
||||
private bool IsCreating = false;
|
||||
private CreateModel Model = new();
|
||||
private string PopupStyle { get; set; } = "";
|
||||
private ElementReference buttonRef;
|
||||
private IJSObjectReference? Module;
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
||||
if (Module is not null)
|
||||
{
|
||||
await Module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await KeyboardShortcutService.RegisterShortcutAsync("gc", ShowPopup);
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/newIdentityWidget.js");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the URL input is focused, place cursor at the end of the default URL to allow for easy typing.
|
||||
/// </summary>
|
||||
private void OnFocusUrlInput(FocusEventArgs e)
|
||||
{
|
||||
if (Model.ServiceUrl != CredentialService.DefaultServiceUrl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a small delay to ensure the focus is set after the browser's default behavior.
|
||||
Task.Delay(1).ContinueWith(_ =>
|
||||
{
|
||||
JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('serviceUrl').setSelectionRange({CredentialService.DefaultServiceUrl.Length}, {CredentialService.DefaultServiceUrl.Length})");
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TogglePopup()
|
||||
{
|
||||
IsPopupVisible = !IsPopupVisible;
|
||||
if (IsPopupVisible)
|
||||
{
|
||||
await ShowPopup();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ShowPopup()
|
||||
{
|
||||
IsPopupVisible = true;
|
||||
|
||||
// Clear the input fields
|
||||
Model = new();
|
||||
Model.ServiceUrl = CredentialService.DefaultServiceUrl;
|
||||
|
||||
await UpdatePopupStyle();
|
||||
await Task.Delay(100); // Give time for the DOM to update
|
||||
await JsInteropService.FocusElementById("serviceName");
|
||||
}
|
||||
|
||||
private void ClosePopup()
|
||||
{
|
||||
IsPopupVisible = false;
|
||||
}
|
||||
|
||||
private async Task UpdatePopupStyle()
|
||||
{
|
||||
var windowWidth = await JSRuntime.InvokeAsync<int>("getWindowWidth");
|
||||
var buttonRect = await JSRuntime.InvokeAsync<BoundingClientRect>("getElementRect", buttonRef);
|
||||
|
||||
var popupWidth = Math.Min(400, windowWidth - 20); // 20px for some padding
|
||||
var leftPosition = Math.Max(0, Math.Min(buttonRect.Left, windowWidth - popupWidth - 10));
|
||||
|
||||
PopupStyle = $"width: {popupWidth}px; left: {leftPosition}px; top: {buttonRect.Bottom}px;";
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task CreateIdentity()
|
||||
{
|
||||
if (IsCreating)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsCreating = true;
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
var credential = new Credential();
|
||||
credential.Alias = new Alias();
|
||||
credential.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
||||
credential.Service = new Service();
|
||||
credential.Service.Name = Model.ServiceName;
|
||||
|
||||
if (Model.ServiceUrl != CredentialService.DefaultServiceUrl)
|
||||
{
|
||||
credential.Service.Url = Model.ServiceUrl;
|
||||
}
|
||||
|
||||
credential.Passwords = new List<Password> { new() };
|
||||
await CredentialService.GenerateRandomIdentity(credential);
|
||||
|
||||
var id = await CredentialService.InsertEntryAsync(credential);
|
||||
if (id == Guid.Empty)
|
||||
{
|
||||
// Error saving.
|
||||
IsCreating = false;
|
||||
GlobalLoadingSpinner.Hide();
|
||||
GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
// No error, add success message.
|
||||
GlobalNotificationService.AddSuccessMessage("Credentials created successfully.");
|
||||
|
||||
NavigationManager.NavigateTo("/credentials/" + id);
|
||||
|
||||
IsCreating = false;
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
ClosePopup();
|
||||
}
|
||||
|
||||
private void OpenAdvancedMode()
|
||||
{
|
||||
NavigationManager.NavigateTo("/credentials/create");
|
||||
ClosePopup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounding client rectangle returned from JavaScript.
|
||||
/// </summary>
|
||||
private sealed class BoundingClientRect
|
||||
{
|
||||
public double Left { get; set; }
|
||||
public double Top { get; set; }
|
||||
public double Right { get; set; }
|
||||
public double Bottom { get; set; }
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Local model for the form with support for validation.
|
||||
/// </summary>
|
||||
private sealed class CreateModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The service name.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Display(Name = "Service Name")]
|
||||
public string ServiceName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The service URL.
|
||||
/// </summary>
|
||||
[Display(Name = "Service URL")]
|
||||
public string ServiceUrl { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
178
src/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor
Normal file
178
src/AliasVault.Client/Main/Components/Widgets/SearchWidget.razor
Normal file
@@ -0,0 +1,178 @@
|
||||
@inject DbService DbService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject KeyboardShortcutService KeyboardShortcutService
|
||||
@inject JsInteropService JsInteropService
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="relative" id="searchWidgetContainer">
|
||||
<input
|
||||
id="searchWidget"
|
||||
type="text"
|
||||
placeholder="Type here to search"
|
||||
autocomplete="off"
|
||||
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
|
||||
@bind-value="SearchTerm"
|
||||
@oninput="SearchTermChanged"
|
||||
@onfocus="OnFocus"
|
||||
@onblur="OnBlur"
|
||||
@onkeydown="HandleKeyDown"/>
|
||||
|
||||
@if (ShowHelpText)
|
||||
{
|
||||
<div class="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 p-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
@if (string.IsNullOrEmpty(SearchTerm))
|
||||
{
|
||||
<p>Type a term to search for, this can be the service name, description or email address.</p>
|
||||
}
|
||||
else if (SearchTerm.Length == 1)
|
||||
{
|
||||
<p>Please type more chars</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p>Searching for "@SearchTerm"</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (ShowResults)
|
||||
{
|
||||
@if (SearchResults.Any())
|
||||
{
|
||||
<div class="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
|
||||
@for (int i = 0; i < SearchResults.Count; i++)
|
||||
{
|
||||
var result = SearchResults[i];
|
||||
<div
|
||||
class="search-result @(i == SelectedIndex ? "bg-gray-100 dark:bg-gray-700" : "") px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
@onclick="() => SelectResult(result)">
|
||||
@result.Service.Name <span class="text-gray-500">(@result.Alias.Email)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="absolute z-10 w-full mt-1 bg-white rounded-md shadow-lg dark:bg-gray-800 text-sm text-gray-600 dark:text-gray-400">
|
||||
<div class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
No results found
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private string SearchTerm { get; set; } = string.Empty;
|
||||
private List<Credential> SearchResults { get; set; } = new();
|
||||
private bool ShowResults => SearchTerm.Length >= 2;
|
||||
private bool ShowHelpText { get; set; }
|
||||
private int SelectedIndex { get; set; } = -1;
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
await KeyboardShortcutService.UnregisterShortcutAsync("gs");
|
||||
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
||||
NavigationManager.LocationChanged -= ResetSearchField;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
await KeyboardShortcutService.RegisterShortcutAsync("gs", FocusSearchField);
|
||||
await KeyboardShortcutService.RegisterShortcutAsync("gf", FocusSearchField);
|
||||
NavigationManager.LocationChanged += ResetSearchField;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFocus()
|
||||
{
|
||||
ShowHelpText = true;
|
||||
}
|
||||
|
||||
private void OnBlur()
|
||||
{
|
||||
ShowHelpText = false;
|
||||
}
|
||||
|
||||
private async Task SearchTermChanged(ChangeEventArgs e)
|
||||
{
|
||||
Console.WriteLine("Search term changed");
|
||||
SearchTerm = e.Value?.ToString() ?? string.Empty;
|
||||
await PerformSearch();
|
||||
}
|
||||
|
||||
private async Task PerformSearch()
|
||||
{
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
if (SearchTerm.Length >= 2)
|
||||
{
|
||||
var searchTerms = SearchTerm.Trim().ToLowerInvariant().Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var query = context.Credentials
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Alias)
|
||||
.AsQueryable();
|
||||
|
||||
foreach (var term in searchTerms)
|
||||
{
|
||||
query = query.Where(x =>
|
||||
(x.Service.Name != null && EF.Functions.Like(x.Service.Name.ToLower(), $"%{term}%")) ||
|
||||
(x.Alias.Email != null && EF.Functions.Like(x.Alias.Email.ToLower(), $"%{term}%"))
|
||||
);
|
||||
}
|
||||
|
||||
SearchResults = await query.Take(10).ToListAsync();
|
||||
|
||||
// Select first entry by default so when pressing enter, the first result is immediately selected.
|
||||
SelectedIndex = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
SearchResults.Clear();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task HandleKeyDown(KeyboardEventArgs e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case "ArrowDown":
|
||||
SelectedIndex = Math.Min(SelectedIndex + 1, SearchResults.Count - 1);
|
||||
break;
|
||||
case "ArrowUp":
|
||||
SelectedIndex = Math.Max(SelectedIndex - 1, -1);
|
||||
break;
|
||||
case "Enter":
|
||||
if (SelectedIndex >= 0 && SelectedIndex < SearchResults.Count)
|
||||
{
|
||||
await SelectResult(SearchResults[SelectedIndex]);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectResult(Credential credential)
|
||||
{
|
||||
await JsInteropService.BlurElementById("searchWidget");
|
||||
NavigationManager.NavigateTo($"/credentials/{credential.Id}");
|
||||
}
|
||||
|
||||
private void ResetSearchField(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
SearchTerm = string.Empty;
|
||||
SearchResults.Clear();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task FocusSearchField()
|
||||
{
|
||||
await JsInteropService.FocusElementById("searchWidget");
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,21 @@
|
||||
@implements IDisposable
|
||||
@inject DbService DbService
|
||||
|
||||
@if (Loading)
|
||||
{
|
||||
<div class="flex items-center justify-center">
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" Spinning="false" />
|
||||
}
|
||||
|
||||
<!--
|
||||
<p>Message: @DbService.GetState().CurrentState.Message</p>
|
||||
<p>Last Updated: @DbService.GetState().CurrentState.LastUpdated</p>
|
||||
-->
|
||||
<div class="ms-2">
|
||||
@if (Loading)
|
||||
{
|
||||
<div class="flex items-center justify-center">
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" />
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<SmallLoadingIndicator Title="@LoadingIndicatorMessage" Spinning="false" />
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool Loading { get; set; } = false;
|
||||
private string Message { get; set; } = "";
|
||||
private string LoadingIndicatorMessage { get; set; } = "";
|
||||
private bool DatabaseLoading { get; set; } = false;
|
||||
|
||||
@@ -27,21 +23,36 @@ else
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
DbService.GetState().StateChanged += OnDatabaseStateChanged;
|
||||
|
||||
UpdateLoadingIndicatorMessage();
|
||||
}
|
||||
|
||||
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
UpdateLoadingIndicatorMessage();
|
||||
|
||||
if (newState.Status == DbServiceState.DatabaseStatus.SavingToServer)
|
||||
{
|
||||
// Show loading indicator for at least 0.5 seconds even if the save operation is faster.
|
||||
Message = "Saving...";
|
||||
await ShowLoadingIndicatorAsync();
|
||||
}
|
||||
}
|
||||
|
||||
LoadingIndicatorMessage = Message + " - " + newState.LastUpdated;
|
||||
private void UpdateLoadingIndicatorMessage()
|
||||
{
|
||||
var currentState = DbService.GetState().CurrentState;
|
||||
|
||||
var message = currentState.Status.ToString();
|
||||
if (currentState.Message != string.Empty)
|
||||
{
|
||||
message = currentState.Message;
|
||||
}
|
||||
|
||||
LoadingIndicatorMessage = "Vault status: " + message + " - " + currentState.LastUpdated;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task ShowLoadingIndicatorAsync()
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,57 @@
|
||||
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
|
||||
@inject NavigationManager NavigationManager
|
||||
@implements IDisposable
|
||||
|
||||
<footer class="md:flex md:items-center md:justify-between px-4 2xl:px-0 py-6 md:py-10">
|
||||
<p class="text-sm text-center text-gray-500 mb-4 md:mb-0">
|
||||
© 2024 AliasVault. All rights reserved.
|
||||
</p>
|
||||
<ul class="flex flex-wrap items-center justify-center">
|
||||
<li><a href="#" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">Terms and conditions</a></li>
|
||||
<li><a href="#" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">License</a></li>
|
||||
<li><a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a></li>
|
||||
<li>
|
||||
<a href="#" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">Terms and conditions</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" class="mr-4 text-sm font-normal text-gray-500 hover:underline md:mr-6 dark:text-gray-400">License</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/lanedirt/AliasVault" target="_blank" class="text-sm font-normal text-gray-500 hover:underline dark:text-gray-400">GitHub</a>
|
||||
</li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<div class="text-center text-gray-400 text-sm pt-4 pb-2">@RandomQuote</div>
|
||||
|
||||
@code {
|
||||
private static readonly string[] Quotes =
|
||||
[
|
||||
"Tip: Use the g+c (go create) keyboard shortcut to quickly create a new identity.",
|
||||
"Tip: Use the g+f (go find) keyboard shortcut to focus the search field.",
|
||||
"Tip: Use the g+h (go home) keyboard shortcut to go to the homepage.",
|
||||
];
|
||||
|
||||
private string RandomQuote = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
NavigationManager.LocationChanged -= RefreshQuote;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnAfterRender(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
RandomQuote = Quotes[Random.Shared.Next(Quotes.Length)];
|
||||
NavigationManager.LocationChanged += RefreshQuote;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows a new random quote.
|
||||
/// </summary>
|
||||
private void RefreshQuote(object? sender, LocationChangedEventArgs e)
|
||||
{
|
||||
RandomQuote = Quotes[Random.Shared.Next(Quotes.Length)];
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
<div class="flex pt-16 overflow-hidden bg-gray-50 dark:bg-gray-900">
|
||||
<div id="main-content" class="relative w-full max-w-screen-2xl mx-auto h-full overflow-y-auto bg-gray-50 dark:bg-gray-900">
|
||||
<main>
|
||||
<GlobalNotificationDisplay />
|
||||
@Body
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<nav class="fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700 py-3 px-4">
|
||||
<div class="flex justify-between items-center max-w-screen-2xl mx-auto">
|
||||
<div class="flex justify-start items-center">
|
||||
<a href="/" class="flex mr-14">
|
||||
<a href="/" class="flex md:mr-10">
|
||||
<img src="/icon-trimmed.png" class="mr-3 h-8" alt="AliasVault Logo">
|
||||
<span class="self-center hidden sm:flex text-2xl font-semibold whitespace-nowrap dark:text-white">AliasVault</span>
|
||||
</a>
|
||||
@@ -15,47 +15,55 @@
|
||||
<NavLink href="/credentials" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Credentials
|
||||
</NavLink>
|
||||
<NavLink href="/emails" class="block text-gray-700 hover:text-primary-700 dark:text-gray-400 dark:hover:text-white" ActiveClass="text-primary-700 dark:text-primary-500" Match="NavLinkMatch.All">
|
||||
Emails
|
||||
</NavLink>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New search box -->
|
||||
<div class="flex-grow mx-4">
|
||||
<SearchWidget />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end items-center lg:order-2">
|
||||
<CreateNewIdentityWidget />
|
||||
|
||||
<DbStatusIndicator />
|
||||
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5">
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
<div id="tooltip-toggle" role="tooltip" class="inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip" data-popper-placement="bottom" style="position: absolute; inset: 0px auto auto 0px; margin: 0px; transform: translate3d(1377px, 60px, 0px);">
|
||||
Toggle dark mode
|
||||
<div class="tooltip-arrow" data-popper-arrow="" style="position: absolute; left: 0px; transform: translate3d(68.5px, 0px, 0px);"></div>
|
||||
</div>
|
||||
|
||||
<button @onclick="ToggleMenu" type="button" class="flex mx-3 text-sm bg-gray-800 rounded-full md:mr-0 flex-shrink-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="userMenuDropdownButton" aria-expanded="false" data-dropdown-toggle="userMenuDropdown">
|
||||
<button @onclick="ToggleMenu" type="button" class="flex ms-3 text-sm bg-gray-800 rounded-full md:mr-0 flex-shrink-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="userMenuDropdownButton" aria-expanded="false" data-dropdown-toggle="userMenuDropdown">
|
||||
<span class="sr-only">Open user menu</span>
|
||||
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="user photo">
|
||||
</button>
|
||||
|
||||
@if (IsMenuOpen)
|
||||
{
|
||||
<div class="absolute top-10 right-0 z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600" id="userMenuDropdown" data-popper-placement="bottom">
|
||||
<div class="py-3 px-4">
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@Username</span>
|
||||
</div>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
<a href="/settings/general" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">General settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings/vault" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Vault settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="absolute top-10 right-0 z-50 my-4 w-56 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700 dark:divide-gray-600 @(IsMenuOpen ? "block" : "hidden")" id="userMenuDropdown" data-popper-placement="bottom">
|
||||
<div class="py-3 px-4">
|
||||
<span class="block text-sm font-semibold text-gray-900 dark:text-white">@Username</span>
|
||||
</div>
|
||||
}
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="userMenuDropdownButton">
|
||||
<li>
|
||||
<a href="/settings/general" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">General settings</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/settings/vault" class="block py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">Vault settings</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<button id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button" class="w-full text-start py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-400 dark:hover:text-white">
|
||||
Toggle dark mode
|
||||
<svg id="theme-toggle-dark-icon" class="hidden w-5 h-5 align-middle inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
|
||||
<svg id="theme-toggle-light-icon" class="hidden w-5 h-5 align-middle inline-block" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" fill-rule="evenodd" clip-rule="evenodd"></path></svg>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="py-1 font-light text-gray-500 dark:text-gray-400" aria-labelledby="dropdown">
|
||||
<li>
|
||||
<a href="/user/logout" class="block py-2 px-4 font-bold text-sm text-primary-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-primary-200 dark:hover:text-white">Sign out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<button @onclick="ToggleMobileMenu" type="button" id="toggleMobileMenuButton" class="items-center p-2 text-gray-500 rounded-lg md:ml-2 lg:hidden hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600">
|
||||
<span class="sr-only">Open menu</span>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
.navbar-toggler {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a {
|
||||
color: #d7d7d7;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep a:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapse {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace AliasVault.Client.Main.Models;
|
||||
/// <summary>
|
||||
/// Represents a breadcrumb item for the breadcrumb component.
|
||||
/// </summary>
|
||||
public class BreadcrumbItem
|
||||
public sealed class BreadcrumbItem
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name for the breadcrumb item.
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace AliasVault.Client.Main.Models;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasClientDb;
|
||||
using AliasVault.Client.Main.Models.FormValidation;
|
||||
@@ -16,7 +17,7 @@ using AliasVault.Client.Main.Models.FormValidation;
|
||||
/// <summary>
|
||||
/// Credential edit model.
|
||||
/// </summary>
|
||||
public class CredentialEdit
|
||||
public sealed class CredentialEdit
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Id of the login.
|
||||
@@ -37,6 +38,7 @@ public class CredentialEdit
|
||||
/// Gets or sets the name of the service.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Display(Name = "Service Name")]
|
||||
public string ServiceName { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace AliasVault.Client.Main.Models;
|
||||
/// <summary>
|
||||
/// Alias list entry model. This model is used to represent an alias in a list with simplified properties.
|
||||
/// </summary>
|
||||
public class CredentialListEntry
|
||||
public sealed class CredentialListEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the alias id.
|
||||
|
||||
@@ -17,7 +17,7 @@ using System.Globalization;
|
||||
/// Initializes a new instance of the <see cref="StringDateFormatAttribute"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="format">The date format to validate.</param>
|
||||
public class StringDateFormatAttribute(string format) : ValidationAttribute
|
||||
public sealed class StringDateFormatAttribute(string format) : ValidationAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Check if the date string is in the correct format.
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
@page "/add-credentials"
|
||||
@page "/credentials/create"
|
||||
@page "/credentials/{id:guid}/edit"
|
||||
@inherits MainBase
|
||||
@inject CredentialService CredentialService
|
||||
@using System.Globalization
|
||||
@using AliasGenerators.Password
|
||||
@using AliasGenerators.Password.Implementations
|
||||
@using System.Text.Json
|
||||
@using System.Text.Json.Serialization
|
||||
@using AliasGenerators.Identity
|
||||
@using AliasGenerators.Identity.Implementations
|
||||
@using AliasGenerators.Identity.Models
|
||||
@inject IJSRuntime JSRuntime
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@if (EditMode)
|
||||
{
|
||||
@@ -25,6 +30,10 @@ else {
|
||||
else {
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Add credentials</h1>
|
||||
}
|
||||
<div>
|
||||
<button type="button" @onclick="TriggerFormSubmit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Credentials</button>
|
||||
<button type="button" @onclick="Cancel" class="px-4 py-2 text-white bg-red-600 rounded-lg hover:bg-red-700 focus:ring-4 focus:ring-red-300 dark:bg-red-500 dark:hover:bg-red-600 dark:focus:ring-red-800">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (EditMode)
|
||||
{
|
||||
@@ -42,11 +51,11 @@ else {
|
||||
}
|
||||
else
|
||||
{
|
||||
<EditForm Model="Obj" OnValidSubmit="SaveAlias">
|
||||
<EditForm @ref="EditFormRef" Model="Obj" OnValidSubmit="SaveAlias">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="grid grid-cols-3 px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
<div class="col-1">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="p-4 mb-4 bg-white border-2 border-primary-600 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Service</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
@@ -54,7 +63,7 @@ else
|
||||
<ValidationMessage For="() => Obj.ServiceName"/>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="service-url" Label="Service URL" @bind-Value="Obj.ServiceUrl"></EditFormRow>
|
||||
<EditFormRow Id="service-url" OnFocus="OnFocusUrlInput" Label="Service URL" @bind-Value="Obj.ServiceUrl"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -64,7 +73,7 @@ else
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Notes</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Type="textarea" Id="first-name" Label="Notes" @bind-Value="Obj.Notes"></EditFormRow>
|
||||
<EditFormRow Type="textarea" Id="notes" Label="Notes" @bind-Value="Obj.Notes"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -87,20 +96,22 @@ else
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Login credentials</h3>
|
||||
<div class="mb-4">
|
||||
<button type="button" class="px-4 py-2 text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800" @onclick="GenerateRandomIdentity">Generate Random Identity</button>
|
||||
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Credentials</button>
|
||||
<button type="button" @onclick="GenerateRandomAlias" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">Generate Random Alias</button>
|
||||
</div>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditEmailFormRow Id="email" Label="Email" @bind-Value="Obj.Alias.Email"></EditEmailFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Username"></EditFormRow>
|
||||
<div class="relative">
|
||||
<EditFormRow Id="username" Label="Username" @bind-Value="Obj.Username"></EditFormRow>
|
||||
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomUsername">New Username</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<div class="relative">
|
||||
<EditFormRow Id="password" Label="Password" @bind-Value="Obj.Password.Value"></EditFormRow>
|
||||
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">(Re)generate Random Password</button>
|
||||
<button type="button" class="text-white absolute end-1 bottom-1 bg-gray-700 hover:bg-gray-800 focus:ring-4 focus:outline-none focus:ring-gray-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-800" @onclick="GenerateRandomPassword">New Password</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +119,7 @@ else
|
||||
|
||||
<div class="col">
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Identity</h3>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Alias</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="first-name" Label="First Name" @bind-Value="Obj.Alias.FirstName"></EditFormRow>
|
||||
@@ -126,41 +137,12 @@ else
|
||||
<EditFormRow Id="birthdate" Label="Birth Date" @bind-Value="Obj.AliasBirthDate"></EditFormRow>
|
||||
<ValidationMessage For="() => Obj.AliasBirthDate"/>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="street" Label="Address Street" @bind-Value="Obj.Alias.AddressStreet"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="city" Label="Address City" @bind-Value="Obj.Alias.AddressCity"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="state" Label="Address State" @bind-Value="Obj.Alias.AddressState"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="zipcode" Label="Address Zip Code" @bind-Value="Obj.Alias.AddressZipCode"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="country" Label="Address Country" @bind-Value="Obj.Alias.AddressCountry"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="hobbies" Label="Hobbies" @bind-Value="Obj.Alias.Hobbies"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="phone-mobile" Label="Phone Mobile" @bind-Value="Obj.Alias.PhoneMobile"></EditFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<EditFormRow Id="iban" Label="Bank Account IBAN" @bind-Value="Obj.Alias.BankAccountIBAN"></EditFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="px-4 py-2 text-white bg-green-600 rounded-lg hover:bg-green-700 focus:ring-4 focus:ring-green-300 dark:bg-green-500 dark:hover:bg-green-600 dark:focus:ring-green-800">Save Credentials</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid px-4 pt-6 lg:gap-4 dark:bg-gray-900">
|
||||
|
||||
</div>
|
||||
<button type="submit" class="hidden">Save Credentials</button>
|
||||
</EditForm>
|
||||
}
|
||||
|
||||
@@ -172,8 +154,20 @@ else
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
private bool EditMode { get; set; }
|
||||
private EditForm EditFormRef { get; set; } = null!;
|
||||
private bool Loading { get; set; } = true;
|
||||
private CredentialEdit Obj { get; set; } = new();
|
||||
private IJSObjectReference? Module;
|
||||
|
||||
/// <inheritdoc />
|
||||
async ValueTask IAsyncDisposable.DisposeAsync()
|
||||
{
|
||||
await KeyboardShortcutService.UnregisterShortcutAsync("gc");
|
||||
if (Module is not null)
|
||||
{
|
||||
await Module.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
@@ -197,7 +191,7 @@ else
|
||||
|
||||
if (EditMode)
|
||||
{
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "View credential", Url = $"/credentials/{Id}" });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "View credentials entry", Url = $"/credentials/{Id}" });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Edit credential" });
|
||||
}
|
||||
else
|
||||
@@ -213,6 +207,7 @@ else
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
Module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./js/modules/newIdentityWidget.js");
|
||||
if (EditMode)
|
||||
{
|
||||
if (Id is null)
|
||||
@@ -234,72 +229,104 @@ else
|
||||
}
|
||||
|
||||
Obj = CredentialToCredentialEdit(alias);
|
||||
if (Obj.ServiceUrl is null)
|
||||
{
|
||||
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new Obj
|
||||
var alias = new Credential();
|
||||
alias.Alias = new Alias();
|
||||
alias.Alias.Email = "@" + GetDefaultEmailDomain();
|
||||
alias.Alias.Email = "@" + CredentialService.GetDefaultEmailDomain();
|
||||
alias.Service = new Service();
|
||||
alias.Passwords = new List<Password> { new Password() };
|
||||
|
||||
Obj = CredentialToCredentialEdit(alias);
|
||||
Obj.ServiceUrl = CredentialService.DefaultServiceUrl;
|
||||
}
|
||||
|
||||
// Hide loading spinner
|
||||
Loading = false;
|
||||
// Force re-render invoke so the charts can be rendered
|
||||
StateHasChanged();
|
||||
|
||||
if (!EditMode)
|
||||
{
|
||||
// When creating a new identity: start with focus on the service name input.
|
||||
await JsInteropService.FocusElementById("service-name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When the URL input is focused, place cursor at the end of the default URL to allow for easy typing.
|
||||
/// </summary>
|
||||
private void OnFocusUrlInput(FocusEventArgs e)
|
||||
{
|
||||
if (Obj.ServiceUrl != CredentialService.DefaultServiceUrl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Use a small delay to ensure the focus is set after the browser's default behavior.
|
||||
Task.Delay(1).ContinueWith(_ =>
|
||||
{
|
||||
JSRuntime.InvokeVoidAsync("eval", $"document.getElementById('service-url').setSelectionRange({CredentialService.DefaultServiceUrl.Length}, {CredentialService.DefaultServiceUrl.Length})");
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleAttachmentsChanged(List<Attachment> updatedAttachments)
|
||||
{
|
||||
Obj.Attachments = updatedAttachments;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task GenerateRandomIdentity()
|
||||
private async Task GenerateRandomAlias()
|
||||
{
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
// Generate a random identity using the IIdentityGenerator implementation.
|
||||
var identity = await CredentialService.GenerateRandomIdentityAsync();
|
||||
|
||||
// Generate random values for the Identity properties
|
||||
Obj.Username = identity.NickName;
|
||||
Obj.Alias.FirstName = identity.FirstName;
|
||||
Obj.Alias.LastName = identity.LastName;
|
||||
Obj.Alias.NickName = identity.NickName;
|
||||
Obj.Alias.Gender = identity.Gender == 1 ? "Male" : "Female";
|
||||
Obj.AliasBirthDate = identity.BirthDate.ToString("yyyy-MM-dd");
|
||||
Obj.Alias.AddressStreet = identity.Address.Street;
|
||||
Obj.Alias.AddressCity = identity.Address.City;
|
||||
Obj.Alias.AddressState = identity.Address.State;
|
||||
Obj.Alias.AddressZipCode = identity.Address.ZipCode;
|
||||
Obj.Alias.AddressCountry = identity.Address.Country;
|
||||
Obj.Alias.Hobbies = identity.Hobbies[0];
|
||||
|
||||
// Set the email
|
||||
var emailDomain = GetDefaultEmailDomain();
|
||||
Obj.Alias.Email = $"{identity.EmailPrefix}@{emailDomain}";
|
||||
Obj.Alias.PhoneMobile = identity.PhoneMobile;
|
||||
Obj.Alias.BankAccountIBAN = identity.BankAccountIBAN;
|
||||
|
||||
// Generate password
|
||||
GenerateRandomPassword();
|
||||
Obj = CredentialToCredentialEdit(await CredentialService.GenerateRandomIdentity(CredentialEditToCredential(Obj)));
|
||||
|
||||
GlobalLoadingSpinner.Hide();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new random username based on existing identity, or if no identity is present,
|
||||
/// generate a new random identity.
|
||||
/// </summary>
|
||||
private async Task GenerateRandomUsername()
|
||||
{
|
||||
// If current object is null, then we create a new random identity.
|
||||
Identity identity;
|
||||
if (Obj.Alias.FirstName is null && Obj.Alias.LastName is null && Obj.Alias.BirthDate == DateTime.MinValue)
|
||||
{
|
||||
identity = await IdentityGeneratorFactory.CreateIdentityGenerator(DbService.Settings.DefaultIdentityLanguage).GenerateRandomIdentityAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assemble identity model with the current values
|
||||
identity = new Identity
|
||||
{
|
||||
FirstName = Obj.Alias.FirstName ?? string.Empty,
|
||||
LastName = Obj.Alias.LastName ?? string.Empty,
|
||||
BirthDate = Obj.Alias.BirthDate,
|
||||
Gender = Obj.Alias.Gender == Gender.Female.ToString() ? Gender.Female : Gender.Male,
|
||||
NickName = Obj.Alias.NickName ?? string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
var generator = new UsernameEmailGenerator();
|
||||
Obj.Username = generator.GenerateUsername(identity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new random password.
|
||||
/// </summary>
|
||||
private void GenerateRandomPassword()
|
||||
{
|
||||
// Generate a random password using a IPasswordGenerator implementation.
|
||||
IPasswordGenerator passwordGenerator = new SpamOkPasswordGenerator();
|
||||
Obj.Password.Value = passwordGenerator.GenerateRandomPassword();
|
||||
Obj.Password.Value = CredentialService.GenerateRandomPassword();
|
||||
}
|
||||
|
||||
private async Task SaveAlias()
|
||||
@@ -307,8 +334,6 @@ else
|
||||
GlobalLoadingSpinner.Show();
|
||||
StateHasChanged();
|
||||
|
||||
// Sanity check for unittest. Delete later if not needed.
|
||||
// Try to parse birthdate as datetime. if it fails, set it to empty.
|
||||
if (EditMode)
|
||||
{
|
||||
if (Id is not null)
|
||||
@@ -344,30 +369,49 @@ else
|
||||
NavigationManager.NavigateTo("/credentials/" + Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a Credential object to a CredentialEdit object.
|
||||
/// </summary>
|
||||
private CredentialEdit CredentialToCredentialEdit(Credential alias)
|
||||
{
|
||||
// Create a deep copy of the alias object to prevent changes to the original object
|
||||
// when editing the alias in the form. We only want to save the changes when the user
|
||||
// clicks the save button.
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
ReferenceHandler = ReferenceHandler.Preserve,
|
||||
MaxDepth = 128 // Adjust this value as needed
|
||||
};
|
||||
|
||||
// Create a deep copy of the credential object
|
||||
var aliasJson = JsonSerializer.Serialize(alias, options);
|
||||
var aliasCopy = JsonSerializer.Deserialize<Credential>(aliasJson, options)!;
|
||||
|
||||
return new CredentialEdit
|
||||
{
|
||||
Id = alias.Id,
|
||||
Notes = alias.Notes ?? string.Empty,
|
||||
Username = alias.Username,
|
||||
ServiceName = alias.Service.Name ?? string.Empty,
|
||||
ServiceUrl = alias.Service.Url,
|
||||
ServiceLogo = alias.Service.Logo,
|
||||
Password = alias.Passwords.FirstOrDefault() ?? new Password
|
||||
Id = aliasCopy.Id,
|
||||
Notes = aliasCopy.Notes ?? string.Empty,
|
||||
Username = aliasCopy.Username ?? string.Empty,
|
||||
ServiceName = aliasCopy.Service.Name ?? string.Empty,
|
||||
ServiceUrl = aliasCopy.Service.Url,
|
||||
ServiceLogo = aliasCopy.Service.Logo,
|
||||
Password = aliasCopy.Passwords.FirstOrDefault() ?? new Password
|
||||
{
|
||||
Value = string.Empty,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
},
|
||||
Alias = alias.Alias,
|
||||
AliasBirthDate = alias.Alias.BirthDate.ToString("yyyy-MM-dd"),
|
||||
Attachments = alias.Attachments.ToList(),
|
||||
CreateDate = alias.CreatedAt,
|
||||
LastUpdate = alias.UpdatedAt
|
||||
Alias = aliasCopy.Alias,
|
||||
AliasBirthDate = aliasCopy.Alias.BirthDate.ToString("yyyy-MM-dd"),
|
||||
Attachments = aliasCopy.Attachments.ToList(),
|
||||
CreateDate = aliasCopy.CreatedAt,
|
||||
LastUpdate = aliasCopy.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method to convert a CredentialEdit object to a Credential object.
|
||||
/// </summary>
|
||||
private Credential CredentialEditToCredential(CredentialEdit alias)
|
||||
{
|
||||
var credential = new Credential()
|
||||
@@ -402,27 +446,23 @@ else
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the default email domain based on settings and available domains.
|
||||
/// Cancel the edit operation and navigate back to the credentials view.
|
||||
/// </summary>
|
||||
private string GetDefaultEmailDomain()
|
||||
private void Cancel()
|
||||
{
|
||||
var defaultDomain = DbService.Settings.DefaultEmailDomain;
|
||||
NavigationManager.NavigateTo("/credentials/" + Id);
|
||||
}
|
||||
|
||||
// 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));
|
||||
/// <summary>
|
||||
/// Trigger the form submit.
|
||||
/// </summary>
|
||||
private async Task TriggerFormSubmit()
|
||||
{
|
||||
if (EditFormRef.EditContext?.Validate() == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
await SaveAlias();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
<Breadcrumb BreadcrumbItems="BreadcrumbItems" />
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Credentials</h1>
|
||||
<a href="/add-credentials" class="px-4 py-2 text-white bg-primary-600 rounded-lg hover:bg-primary-700 focus:ring-4 focus:ring-primary-300 dark:bg-primary-500 dark:hover:bg-primary-600 dark:focus:ring-primary-800">
|
||||
+ Add new credentials
|
||||
</a>
|
||||
<RefreshButton OnRefresh="LoadCredentialsAsync" ButtonText="Refresh" />
|
||||
</div>
|
||||
<p>Find all of your credentials below.</p>
|
||||
</div>
|
||||
@@ -22,38 +20,17 @@
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
@if (Credentials.Count == 0)
|
||||
{
|
||||
<div class="p-4 mx-4 mt-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="px-4 py-2 text-gray-400 rounded">
|
||||
<p class="text-gray-500 dark:text-gray-400">You have no credentials yet. Create your first one now by clicking on the + button in the top right corner.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="grid gap-4 px-4 mb-4 md:grid-cols-4 xl:grid-cols-6">
|
||||
@foreach (var credential in Credentials)
|
||||
{
|
||||
<CredentialCard Obj="@credential"/>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="grid gap-4 px-4 mb-4 md:grid-cols-4 xl:grid-cols-6">
|
||||
@foreach (var credential in Credentials)
|
||||
{
|
||||
<CredentialCard Obj="@credential"/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private List<CredentialListEntry> Credentials { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Redirect to /credentials.
|
||||
NavigationManager.NavigateTo("/credentials");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
@@ -78,6 +55,11 @@ else
|
||||
GlobalNotificationService.AddErrorMessage("Failed to load credentials.", true);
|
||||
return;
|
||||
}
|
||||
if (credentialListEntries.Count == 0)
|
||||
{
|
||||
// Redirect to welcome page.
|
||||
NavigationManager.NavigateTo("/welcome");
|
||||
}
|
||||
|
||||
Credentials = credentialListEntries;
|
||||
IsLoading = false;
|
||||
|
||||
@@ -74,11 +74,11 @@ else
|
||||
</form>
|
||||
</div>
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Name</h3>
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Alias</h3>
|
||||
<form action="#">
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="Initials" Value="@(Alias.Alias.FirstName?.Substring(0,1))"></CopyPasteFormRow>
|
||||
<div class="col-span-6">
|
||||
<CopyPasteFormRow Label="Full name" Value="@(Alias.Alias.FirstName + " " + Alias.Alias.LastName)"></CopyPasteFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="First name" Value="@(Alias.Alias.FirstName)"></CopyPasteFormRow>
|
||||
@@ -95,40 +95,6 @@ else
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Contact</h3>
|
||||
<form action="#">
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="Phone" Value="@(Alias.Alias.PhoneMobile)"></CopyPasteFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="IBAN" Value="@(Alias.Alias.BankAccountIBAN)"></CopyPasteFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Address</h3>
|
||||
<form action="#">
|
||||
<div class="grid grid-cols-6 gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="Street" Value="@(Alias.Alias.AddressStreet)"></CopyPasteFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="Postal code" Value="@(Alias.Alias.AddressZipCode)"></CopyPasteFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="City" Value="@(Alias.Alias.AddressCity)"></CopyPasteFormRow>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<CopyPasteFormRow Label="Country" Value="@(Alias.Alias.AddressCountry)"></CopyPasteFormRow>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -150,16 +116,15 @@ else
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadEntryAsync();
|
||||
}
|
||||
await base.OnParametersSetAsync();
|
||||
await LoadEntryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the credentials entry.
|
||||
/// </summary>
|
||||
private async Task LoadEntryAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
|
||||
282
src/AliasVault.Client/Main/Pages/Emails/Home.razor
Normal file
282
src/AliasVault.Client/Main/Pages/Emails/Home.razor
Normal file
@@ -0,0 +1,282 @@
|
||||
@page "/emails"
|
||||
@using System.Net
|
||||
@using System.Text
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Client.Main.Pages.Emails.Models
|
||||
@using AliasVault.Shared.Models.Spamok
|
||||
@using AliasVault.Shared.Models.WebApi
|
||||
@using AliasVault.Shared.Models.WebApi.Email
|
||||
@inherits MainBase
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<LayoutPageTitle>Emails</LayoutPageTitle>
|
||||
|
||||
@if (EmailModalVisible)
|
||||
{
|
||||
<EmailModal Email="EmailModalEmail" IsSpamOk="false" OnClose="CloseEmailModal" />
|
||||
}
|
||||
|
||||
<div class="grid grid-cols-1 px-4 pt-6 xl:grid-cols-3 xl:gap-4 dark:bg-gray-900">
|
||||
<div class="mb-4 col-span-full xl:mb-2">
|
||||
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">Emails</h1>
|
||||
<RefreshButton OnRefresh="RefreshData" ButtonText="Refresh" />
|
||||
</div>
|
||||
<p>Below you can find all recent emails sent to one of the email addresses used in your credentials.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator/>
|
||||
}
|
||||
else if (NoEmailClaims)
|
||||
{
|
||||
<div class="p-4 mx-4 mt-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="px-4 py-2 text-gray-400 rounded">
|
||||
<p class="text-gray-500 dark:text-gray-400">You are not using any private email addresses (yet). Create a new identity and use a private email address supported by AliasVault. All emails received by these private email addresses will show up here.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="overflow-x-auto px-4">
|
||||
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged"/>
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden mt-6">
|
||||
<div class="px-4 py-2 bg-gray-100 border-b">
|
||||
<h2 class="font-semibold text-gray-800">Inbox</h2>
|
||||
</div>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
@if (EmailList.Count == 0)
|
||||
{
|
||||
<li class="p-4 text-center text-gray-500">
|
||||
No emails have been received yet.
|
||||
</li>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var email in EmailList)
|
||||
{
|
||||
<li class="hover:bg-gray-50 transition duration-150 ease-in-out">
|
||||
<div @onclick="() => ShowAliasVaultEmailInModal(email.Id)" class="p-4 flex justify-start items-start">
|
||||
<div class="mr-4 flex-shrink-0">
|
||||
<SenderInitials SenderName="@email.FromName" SenderEmail="@email.FromEmail" />
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex items-center justify-between mb-2 mr-4">
|
||||
<div>
|
||||
<div class="text-gray-800 mb-2">
|
||||
@email.Subject
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 line-clamp-2">
|
||||
@email.MessagePreview
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div @onclick="() => NavigateToCredential(email.CredentialId)" class="text-sm text-gray-700 cursor-pointer mr-4 hover:underline">@email.CredentialName</div>
|
||||
<div class="text-sm text-gray-500">@email.Date.ToString("yyyy-MM-dd")</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<MailListViewModel> EmailList { get; set; } = [];
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private int CurrentPage { get; set; } = 1;
|
||||
private int PageSize { get; set; } = 50;
|
||||
private int TotalRecords { get; set; }
|
||||
private bool EmailModalVisible { get; set; }
|
||||
private bool NoEmailClaims { get; set; }
|
||||
private EmailApiModel EmailModalEmail { get; set; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await RefreshData();
|
||||
}
|
||||
}
|
||||
|
||||
private void HandlePageChanged(int newPage)
|
||||
{
|
||||
CurrentPage = newPage;
|
||||
_ = RefreshData();
|
||||
}
|
||||
|
||||
private async Task RefreshData()
|
||||
{
|
||||
IsLoading = true;
|
||||
NoEmailClaims = false;
|
||||
StateHasChanged();
|
||||
|
||||
var emailClaimList = await DbService.GetEmailClaimListAsync();
|
||||
|
||||
if (emailClaimList.Count == 0)
|
||||
{
|
||||
IsLoading = false;
|
||||
NoEmailClaims = true;
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new MailboxBulkRequest
|
||||
{
|
||||
Page = CurrentPage,
|
||||
PageSize = PageSize,
|
||||
Addresses = emailClaimList,
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"api/v1/EmailBox/bulk");
|
||||
request.Content = new StringContent(JsonSerializer.Serialize(requestModel), Encoding.UTF8, "application/json");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await HttpClient.SendAsync(request);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var mailbox = await response.Content.ReadFromJsonAsync<MailboxBulkResponse>();
|
||||
await UpdateMailboxEmails(mailbox);
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
var errorResponse = JsonSerializer.Deserialize<ApiErrorResponse>(errorContent);
|
||||
switch (response.StatusCode)
|
||||
{
|
||||
case HttpStatusCode.BadRequest:
|
||||
if (errorResponse != null)
|
||||
{
|
||||
switch (errorResponse.Code)
|
||||
{
|
||||
case "CLAIM_DOES_NOT_EXIST":
|
||||
GlobalNotificationService.AddErrorMessage("An error occurred while trying to load the emails. Please try to edit and " +
|
||||
"save any credential entry to synchronize the database, then try again.", true);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException(errorResponse.Message);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
case HttpStatusCode.Unauthorized:
|
||||
throw new UnauthorizedAccessException(errorResponse?.Message);
|
||||
default:
|
||||
throw new WebException(errorResponse?.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
GlobalNotificationService.AddErrorMessage(ex.Message, true);
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the local mailbox emails.
|
||||
/// </summary>
|
||||
private async Task UpdateMailboxEmails(MailboxBulkResponse? model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
EmailList = [];
|
||||
TotalRecords = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
|
||||
// Fetch all credentials in a single query and create a lookup dictionary
|
||||
var credentialLookup = await context.Credentials
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Alias)
|
||||
.Where(x => x.Alias.Email != null)
|
||||
.GroupBy(x => x.Alias.Email!.ToLower())
|
||||
.ToDictionaryAsync(
|
||||
g => g.Key,
|
||||
g => new { Id = g.First().Id, ServiceName = g.First().Service.Name ?? "Unknown" }
|
||||
);
|
||||
|
||||
// Convert the email list to view models and add credential info in a single pass
|
||||
var decryptedEmailList = await EmailService.DecryptEmailList(model.Mails);
|
||||
EmailList = decryptedEmailList.Select(email =>
|
||||
{
|
||||
var toEmail = email.ToLocal + "@" + email.ToDomain;
|
||||
var credentialInfo = credentialLookup.TryGetValue(toEmail.ToLower(), out var info)
|
||||
? info
|
||||
: new { Id = Guid.Empty, ServiceName = "Unknown" };
|
||||
|
||||
return new MailListViewModel
|
||||
{
|
||||
Id = email.Id,
|
||||
Date = email.DateSystem,
|
||||
FromName = email.FromDisplay,
|
||||
FromEmail = email.FromLocal + "@" + email.FromDomain,
|
||||
ToEmail = toEmail,
|
||||
Subject = email.Subject,
|
||||
MessagePreview = email.MessagePreview,
|
||||
CredentialId = credentialInfo.Id,
|
||||
CredentialName = credentialInfo.ServiceName
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
CurrentPage = model.CurrentPage;
|
||||
PageSize = model.PageSize;
|
||||
TotalRecords = model.TotalRecords;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load recent emails from AliasVault.
|
||||
/// </summary>
|
||||
private async Task ShowAliasVaultEmailInModal(int emailId)
|
||||
{
|
||||
EmailApiModel? mail = await HttpClient.GetFromJsonAsync<EmailApiModel>($"api/v1/Email/{emailId}");
|
||||
if (mail != null)
|
||||
{
|
||||
// Decrypt the email content locally.
|
||||
var context = await DbService.GetDbContextAsync();
|
||||
var privateKey = await context.EncryptionKeys.FirstOrDefaultAsync(x => x.PublicKey == mail.EncryptionKey);
|
||||
if (privateKey is not null)
|
||||
{
|
||||
mail = await EmailService.DecryptEmail(mail);
|
||||
}
|
||||
|
||||
EmailModalEmail = mail;
|
||||
EmailModalVisible = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Close the email modal.
|
||||
/// </summary>
|
||||
private void CloseEmailModal()
|
||||
{
|
||||
EmailModalVisible = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the credential page.
|
||||
/// </summary>
|
||||
private void NavigateToCredential(Guid credentialId)
|
||||
{
|
||||
NavigationManager.NavigateTo($"/credentials/{credentialId}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MailListViewModel.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.Main.Pages.Emails.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Mail view model.
|
||||
/// </summary>
|
||||
public class MailListViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the email.
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the associated credential that uses this email address.
|
||||
/// </summary>
|
||||
public Guid CredentialId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the credential that uses this email address.
|
||||
/// </summary>
|
||||
public string CredentialName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subject of the email.
|
||||
/// </summary>
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the sender.
|
||||
/// </summary>
|
||||
public string FromName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sender's email address.
|
||||
/// </summary>
|
||||
public string FromEmail { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the recipients email address.
|
||||
/// </summary>
|
||||
public string ToEmail { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date of the email.
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the message preview.
|
||||
/// </summary>
|
||||
public string MessagePreview { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -60,6 +60,18 @@ public class MainBase : OwningComponentBase
|
||||
[Inject]
|
||||
public DbService DbService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the EmailService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public EmailService EmailService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the KeyboardShortcutService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public KeyboardShortcutService KeyboardShortcutService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AuthService.
|
||||
/// </summary>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Email Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
@@ -44,6 +44,21 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mx-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Alias Settings</h3>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="defaultEmailDomain" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Alias generation language</label>
|
||||
<select @bind="DefaultIdentityLanguage" @bind:after="UpdateDefaultIdentityLanguage" id="defaultIdentityLanguage" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
|
||||
<option value="en">English</option>
|
||||
<option value="nl">Dutch</option>
|
||||
</select>
|
||||
<span class="block text-sm font-normal text-gray-500 truncate dark:text-gray-400">
|
||||
Set the default language that will be used when generating new identities.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private List<string> PrivateDomains => Config.PrivateEmailDomains;
|
||||
private List<string> PublicDomains => Config.PublicEmailDomains;
|
||||
@@ -51,15 +66,28 @@
|
||||
|
||||
private string DefaultEmailDomain { get; set; } = string.Empty;
|
||||
private bool AutoEmailRefresh { get; set; }
|
||||
private string DefaultIdentityLanguage { get; set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "Vault settings" });
|
||||
BreadcrumbItems.Add(new BreadcrumbItem { DisplayName = "General settings" });
|
||||
|
||||
DefaultEmailDomain = DbService.Settings.DefaultEmailDomain;
|
||||
if (DefaultEmailDomain == string.Empty)
|
||||
{
|
||||
if (PrivateDomains.Count > 0)
|
||||
{
|
||||
DefaultEmailDomain = PrivateDomains[0];
|
||||
}
|
||||
else if (PublicDomains.Count > 0)
|
||||
{
|
||||
DefaultEmailDomain = PublicDomains[0];
|
||||
}
|
||||
}
|
||||
AutoEmailRefresh = DbService.Settings.AutoEmailRefresh;
|
||||
DefaultIdentityLanguage = DbService.Settings.DefaultIdentityLanguage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,4 +107,13 @@
|
||||
await DbService.Settings.SetAutoEmailRefresh(AutoEmailRefresh);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the auto email refresh setting.
|
||||
/// </summary>
|
||||
private async Task UpdateDefaultIdentityLanguage()
|
||||
{
|
||||
await DbService.Settings.SetDefaultIdentityLanguage(DefaultIdentityLanguage);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="p-4 mx-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Export vault</h3>
|
||||
<div class="mb-4">
|
||||
<div>
|
||||
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<div class="p-4 mx-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Import vault</h3>
|
||||
<div class="mb-4">
|
||||
<div>
|
||||
@@ -117,9 +117,13 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
|
||||
{
|
||||
var file = e.File;
|
||||
var buffer = new byte[file.Size];
|
||||
await file.OpenReadStream().ReadAsync(buffer);
|
||||
var fileContent = System.Text.Encoding.UTF8.GetString(buffer);
|
||||
var bytesRead = await file.OpenReadStream().ReadAsync(buffer);
|
||||
if (bytesRead != file.Size)
|
||||
{
|
||||
throw new FileLoadException("Error reading file");
|
||||
}
|
||||
|
||||
var fileContent = System.Text.Encoding.UTF8.GetString(buffer);
|
||||
var importedCredentials = CsvImportExport.CredentialCsvService.ImportCredentialsFromCsv(fileContent);
|
||||
|
||||
// Loop through the imported credentials and actually add them to the database
|
||||
@@ -131,7 +135,7 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
|
||||
// Save the database.
|
||||
await DbService.SaveDatabaseAsync();
|
||||
|
||||
ImportSuccessMessage = $"Succesfully imported {importedCredentials.Count} credentials.";
|
||||
ImportSuccessMessage = $"Successfully imported {importedCredentials.Count} credentials.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Welcome to AliasVault!</h2>
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">Welcome to AliasVault</h2>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
Your new encrypted vault is being created. This process may take a moment. Please wait.
|
||||
Your new encrypted vault is being initialized. This process may take a moment. Please wait.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@@ -47,13 +47,12 @@
|
||||
private async Task MigrateDatabase()
|
||||
{
|
||||
// Simulate a delay.
|
||||
await Task.Delay(1500);
|
||||
await Task.Delay(1000);
|
||||
|
||||
// Migrate the database
|
||||
if (await DbService.MigrateDatabaseAsync())
|
||||
{
|
||||
// Migration successful
|
||||
GlobalNotificationService.AddSuccessMessage("Vault successfully created.", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
60
src/AliasVault.Client/Main/Pages/Welcome.razor
Normal file
60
src/AliasVault.Client/Main/Pages/Welcome.razor
Normal file
@@ -0,0 +1,60 @@
|
||||
@page "/welcome"
|
||||
@inherits MainBase
|
||||
|
||||
<div class="container pt-10 px-4 mx-auto lg:px-0">
|
||||
<h1 class="mb-3 text-3xl font-bold text-gray-900 sm:text-4xl sm:leading-none sm:tracking-tight dark:text-white">Welcome to AliasVault</h1>
|
||||
<p class="mb-6 text-lg font-normal text-gray-500 sm:text-xl dark:text-gray-400">It looks like you are new here. The instructions on this page will help to get you started.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">Getting Started</h2>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">How do I use AliasVault?</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">Create a random identity with an associated email address. To get started, simply click the "+ New Identity" button in the top right corner.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">What is the purpose of AliasVault?</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">AliasVault keeps you safe online by providing unique passwords and email addresses for every service you use. This prevents your actual email from being used to link your accounts on different websites together.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">Is my data secure?</h3>
|
||||
<p class="text-gray-600 dark:text-gray-400">Yes, AliasVault uses a zero-knowledge architecture. This means that your data is end-to-end encrypted on your device before being stored on the server. No one can see your login information, identities and even emails but you. Not even us.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-md">
|
||||
<h2 class="text-2xl font-semibold mb-4 text-gray-900 dark:text-white">Get Started Now</h2>
|
||||
<div class="mb-4">
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
Go ahead and create a new login by clicking "+ New Identity" in the top right. Or explore these options:
|
||||
</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<button @onclick="CreateIdentity" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition duration-300">
|
||||
Create First Identity (Advanced)
|
||||
</button>
|
||||
<button @onclick="ShowSettings" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded transition duration-300">
|
||||
Choose Default Language for New Identities
|
||||
</button>
|
||||
<button class="w-full bg-gray-400 hover:bg-gray-500 text-white font-bold py-2 px-4 rounded transition duration-300 cursor-not-allowed opacity-75">
|
||||
Import Passwords from Existing Manager (coming soon)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private void CreateIdentity()
|
||||
{
|
||||
NavigationManager.NavigateTo("credentials/create");
|
||||
}
|
||||
|
||||
private void ShowSettings()
|
||||
{
|
||||
NavigationManager.NavigateTo("settings/general");
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ builder.Services.AddLogging(logging =>
|
||||
|
||||
logging.AddFilter("Microsoft.AspNetCore.Identity.DataProtectorTokenProvider", LogLevel.Error);
|
||||
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
|
||||
logging.AddFilter("System.Net.Http.HttpClient", LogLevel.Error);
|
||||
});
|
||||
|
||||
builder.RootComponents.Add<App>("#app");
|
||||
@@ -67,7 +68,9 @@ builder.Services.AddScoped<CredentialService>();
|
||||
builder.Services.AddScoped<DbService>();
|
||||
builder.Services.AddScoped<GlobalNotificationService>();
|
||||
builder.Services.AddScoped<GlobalLoadingService>();
|
||||
builder.Services.AddScoped<KeyboardShortcutService>();
|
||||
builder.Services.AddScoped<JsInteropService>();
|
||||
builder.Services.AddScoped<EmailService>();
|
||||
builder.Services.AddSingleton<ClipboardCopyService>();
|
||||
|
||||
builder.Services.AddAuthorizationCore();
|
||||
|
||||
@@ -15,7 +15,7 @@ using Microsoft.AspNetCore.Components;
|
||||
/// This services handles all API requests to the AliasVault API and will add the access token to the request headers.
|
||||
/// If a 401 unauthorized is returned by the API it will intercept this response and attempt to automatically refresh the access token.
|
||||
/// </summary>
|
||||
public class AliasVaultApiHandlerService(IServiceProvider serviceProvider) : DelegatingHandler
|
||||
public sealed class AliasVaultApiHandlerService(IServiceProvider serviceProvider) : DelegatingHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Override the SendAsync method to add the access token to the request headers.
|
||||
|
||||
@@ -17,14 +17,11 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
/// This service is responsible for handling authentication-related operations such as refreshing tokens,
|
||||
/// storing tokens, and revoking tokens.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Initializes a new instance of the <see cref="AuthService"/> class.
|
||||
/// </remarks>
|
||||
/// <param name="httpClient">The HTTP client.</param>
|
||||
/// <param name="localStorage">The local storage service.</param>
|
||||
/// <param name="environment">IWebAssemblyHostEnvironment instance.</param>
|
||||
/// <param name="configuration">IConfiguration instance.</param>
|
||||
public class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, IConfiguration configuration)
|
||||
public sealed class AuthService(HttpClient httpClient, ILocalStorageService localStorage, IWebAssemblyHostEnvironment environment, IConfiguration configuration)
|
||||
{
|
||||
private const string AccessTokenKey = "token";
|
||||
private const string RefreshTokenKey = "refreshToken";
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace AliasVault.Client.Services;
|
||||
/// <summary>
|
||||
/// Service to manage the clipboard copy operations across the application.
|
||||
/// </summary>
|
||||
public class ClipboardCopyService
|
||||
public sealed class ClipboardCopyService
|
||||
{
|
||||
private string _currentCopiedId = string.Empty;
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ 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.Implementations;
|
||||
using AliasVault.Shared.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Identity = AliasGenerators.Identity.Models.Identity;
|
||||
@@ -21,8 +24,79 @@ using Identity = AliasGenerators.Identity.Models.Identity;
|
||||
/// <summary>
|
||||
/// Service class for alias operations.
|
||||
/// </summary>
|
||||
public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
public sealed class CredentialService(HttpClient httpClient, DbService dbService, Config config)
|
||||
{
|
||||
/// <summary>
|
||||
/// The default service URL used as placeholder in forms. When this value is set, the URL field is considered empty
|
||||
/// and a null value is stored in the database.
|
||||
/// </summary>
|
||||
public const string DefaultServiceUrl = "https://";
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random password for a credential.
|
||||
/// </summary>
|
||||
/// <returns>Random password.</returns>
|
||||
public static string GenerateRandomPassword()
|
||||
{
|
||||
// Generate a random password using a IPasswordGenerator implementation.
|
||||
var passwordGenerator = new SpamOkPasswordGenerator();
|
||||
return passwordGenerator.GenerateRandomPassword();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Generate random identity by calling the IdentityGenerator API.
|
||||
/// </summary>
|
||||
@@ -51,6 +125,19 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
// Try to extract favicon from service URL
|
||||
await ExtractFaviconAsync(loginObject);
|
||||
|
||||
// If the email starts with an @ it is most likely still the placeholder which hasn't been filled.
|
||||
// So we remove it.
|
||||
if (loginObject.Alias.Email is not null && loginObject.Alias.Email.StartsWith('@'))
|
||||
{
|
||||
loginObject.Alias.Email = null;
|
||||
}
|
||||
|
||||
// If the URL equals the placeholder, we set it to null.
|
||||
if (loginObject.Service.Url == "https://")
|
||||
{
|
||||
loginObject.Service.Url = null;
|
||||
}
|
||||
|
||||
var login = new Credential
|
||||
{
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
@@ -64,15 +151,7 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
LastName = loginObject.Alias.LastName,
|
||||
BirthDate = loginObject.Alias.BirthDate,
|
||||
Gender = loginObject.Alias.Gender,
|
||||
AddressStreet = loginObject.Alias.AddressStreet,
|
||||
AddressCity = loginObject.Alias.AddressCity,
|
||||
AddressState = loginObject.Alias.AddressState,
|
||||
AddressZipCode = loginObject.Alias.AddressZipCode,
|
||||
AddressCountry = loginObject.Alias.AddressCountry,
|
||||
Hobbies = loginObject.Alias.Hobbies,
|
||||
Email = loginObject.Alias.Email,
|
||||
PhoneMobile = loginObject.Alias.PhoneMobile,
|
||||
BankAccountIBAN = loginObject.Alias.BankAccountIBAN,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
},
|
||||
@@ -128,6 +207,13 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
throw new InvalidOperationException("Login object not found.");
|
||||
}
|
||||
|
||||
// If the email starts with an @ it is most likely still the placeholder which hasn't been filled.
|
||||
// So we remove it.
|
||||
if (loginObject.Alias.Email is not null && loginObject.Alias.Email.StartsWith('@'))
|
||||
{
|
||||
loginObject.Alias.Email = null;
|
||||
}
|
||||
|
||||
login.UpdatedAt = DateTime.UtcNow;
|
||||
login.Notes = loginObject.Notes;
|
||||
login.Username = loginObject.Username;
|
||||
@@ -137,15 +223,7 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
login.Alias.LastName = loginObject.Alias.LastName;
|
||||
login.Alias.BirthDate = loginObject.Alias.BirthDate;
|
||||
login.Alias.Gender = loginObject.Alias.Gender;
|
||||
login.Alias.AddressStreet = loginObject.Alias.AddressStreet;
|
||||
login.Alias.AddressCity = loginObject.Alias.AddressCity;
|
||||
login.Alias.AddressState = loginObject.Alias.AddressState;
|
||||
login.Alias.AddressZipCode = loginObject.Alias.AddressZipCode;
|
||||
login.Alias.AddressCountry = loginObject.Alias.AddressCountry;
|
||||
login.Alias.Hobbies = loginObject.Alias.Hobbies;
|
||||
login.Alias.Email = loginObject.Alias.Email;
|
||||
login.Alias.PhoneMobile = loginObject.Alias.PhoneMobile;
|
||||
login.Alias.BankAccountIBAN = loginObject.Alias.BankAccountIBAN;
|
||||
|
||||
login.Passwords = loginObject.Passwords;
|
||||
|
||||
@@ -259,6 +337,20 @@ public class CredentialService(HttpClient httpClient, DbService dbService)
|
||||
.FirstAsync();
|
||||
|
||||
context.Credentials.Remove(login);
|
||||
|
||||
// Also remove associated alias and service. Later when
|
||||
// aliases and services are shared between credentials, this
|
||||
// should be removed.
|
||||
var alias = await context.Aliases
|
||||
.Where(x => x.Id == login.Alias.Id)
|
||||
.FirstAsync();
|
||||
context.Aliases.Remove(alias);
|
||||
|
||||
var service = await context.Services
|
||||
.Where(x => x.Id == login.Service.Id)
|
||||
.FirstAsync();
|
||||
context.Services.Remove(service);
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
await dbService.SaveDatabaseAsync();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
/// with a AliasClientDb database instance that is only persisted in memory due to the encryption requirements of the
|
||||
/// database itself. The database should not be persisted to disk when in un-encrypted form.
|
||||
/// </summary>
|
||||
public class DbService : IDisposable
|
||||
public sealed class DbService : IDisposable
|
||||
{
|
||||
private readonly AuthService _authService;
|
||||
private readonly JsInteropService _jsInteropService;
|
||||
@@ -288,7 +288,7 @@ public class DbService : IDisposable
|
||||
/// Disposes the service.
|
||||
/// </summary>
|
||||
/// <param name="disposing">True if disposing.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
public void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
@@ -303,6 +303,29 @@ public class DbService : IDisposable
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of private email addresses that are used in aliases by this vault.
|
||||
/// </summary>
|
||||
/// <returns>List of email addresses.</returns>
|
||||
public async Task<List<string>> GetEmailClaimListAsync()
|
||||
{
|
||||
// Send list of email addresses that are used in aliases by this vault so they can be
|
||||
// claimed on the server.
|
||||
var emailAddresses = await _dbContext.Aliases
|
||||
.Where(a => a.Email != null)
|
||||
.Select(a => a.Email)
|
||||
.Distinct()
|
||||
.Select(email => email!)
|
||||
.ToListAsync();
|
||||
|
||||
// Filter the list of email addresses to only include those that are in the allowed domains.
|
||||
emailAddresses = emailAddresses
|
||||
.Where(email => _config.PrivateEmailDomains.Exists(domain => email.EndsWith(domain)))
|
||||
.ToList();
|
||||
|
||||
return emailAddresses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a SQLite database from a base64 string which represents a .sqlite file.
|
||||
/// </summary>
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace AliasVault.Client.Services.Database;
|
||||
/// <summary>
|
||||
/// Class to manage the state of the AliasClientDbService that others can subscribe to events for.
|
||||
/// </summary>
|
||||
public class DbServiceState
|
||||
public sealed class DbServiceState
|
||||
{
|
||||
private DatabaseState _currentState = new();
|
||||
|
||||
@@ -114,7 +114,7 @@ public class DbServiceState
|
||||
/// OnStateChanged event handler.
|
||||
/// </summary>
|
||||
/// <param name="newState">The new state.</param>
|
||||
protected virtual void OnStateChanged(DatabaseState newState)
|
||||
private void OnStateChanged(DatabaseState newState)
|
||||
{
|
||||
StateChanged?.Invoke(this, newState);
|
||||
}
|
||||
|
||||
111
src/AliasVault.Client/Services/EmailService.cs
Normal file
111
src/AliasVault.Client/Services/EmailService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="EmailService.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 AliasClientDb;
|
||||
using AliasVault.Shared.Models.Spamok;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Email service that contains utility methods for handling email functionality such as client-side decryption.
|
||||
/// </summary>
|
||||
public sealed class EmailService(DbService dbService, JsInteropService jsInteropService, GlobalNotificationService globalNotificationService)
|
||||
{
|
||||
private List<EncryptionKey> _encryptionKeys = [];
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts a single email using the private key.
|
||||
/// </summary>
|
||||
/// <param name="email">The email object with encrypted fields.</param>
|
||||
/// <returns>Email with all fields decrypted.</returns>
|
||||
public async Task<EmailApiModel> DecryptEmail(EmailApiModel email)
|
||||
{
|
||||
await EnsureEncryptionKeys();
|
||||
return await DecryptSingleEmail(email);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts a list of emails using the private key.
|
||||
/// </summary>
|
||||
/// <param name="emailList">The email object with encrypted fields.</param>
|
||||
/// <returns>List of emails with all fields decrypted.</returns>
|
||||
public async Task<List<MailboxEmailApiModel>> DecryptEmailList(List<MailboxEmailApiModel> emailList)
|
||||
{
|
||||
await EnsureEncryptionKeys();
|
||||
|
||||
foreach (var email in emailList)
|
||||
{
|
||||
var privateKey = _encryptionKeys.First(x => x.PublicKey == email.EncryptionKey);
|
||||
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
email.Subject = await jsInteropService.SymmetricDecrypt(email.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
email.FromDisplay = await jsInteropService.SymmetricDecrypt(email.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
email.FromLocal = await jsInteropService.SymmetricDecrypt(email.FromLocal, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
email.FromDomain = await jsInteropService.SymmetricDecrypt(email.FromDomain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
email.MessagePreview = await jsInteropService.SymmetricDecrypt(email.MessagePreview, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
globalNotificationService.AddErrorMessage(ex.Message, true);
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
}
|
||||
|
||||
return emailList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypt the contents of a single email.
|
||||
/// </summary>
|
||||
/// <param name="email">The email object with encrypted fields.</param>
|
||||
/// <returns>Email with all fields decrypted.</returns>
|
||||
private async Task<EmailApiModel> DecryptSingleEmail(EmailApiModel email)
|
||||
{
|
||||
var privateKey = _encryptionKeys.First(x => x.PublicKey == email.EncryptionKey);
|
||||
|
||||
try
|
||||
{
|
||||
var decryptedSymmetricKey = await jsInteropService.DecryptWithPrivateKey(email.EncryptedSymmetricKey, privateKey.PrivateKey);
|
||||
email.Subject = await jsInteropService.SymmetricDecrypt(email.Subject, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
if (email.MessageHtml is not null)
|
||||
{
|
||||
email.MessageHtml = await jsInteropService.SymmetricDecrypt(email.MessageHtml, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
|
||||
if (email.MessagePlain is not null)
|
||||
{
|
||||
email.MessagePlain = await jsInteropService.SymmetricDecrypt(email.MessagePlain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
|
||||
email.FromDisplay = await jsInteropService.SymmetricDecrypt(email.FromDisplay, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
email.FromLocal = await jsInteropService.SymmetricDecrypt(email.FromLocal, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
email.FromDomain = await jsInteropService.SymmetricDecrypt(email.FromDomain, Convert.ToBase64String(decryptedSymmetricKey));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
globalNotificationService.AddErrorMessage(ex.Message, true);
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure the encryption keys are loaded.
|
||||
/// </summary>
|
||||
private async Task EnsureEncryptionKeys()
|
||||
{
|
||||
if (_encryptionKeys.Count == 0)
|
||||
{
|
||||
var context = await dbService.GetDbContextAsync();
|
||||
_encryptionKeys = await context.EncryptionKeys.ToListAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ namespace AliasVault.Client.Services;
|
||||
/// <summary>
|
||||
/// Global loading service that can be used to show or hide a global layout loading spinner.
|
||||
/// </summary>
|
||||
public class GlobalLoadingService
|
||||
public sealed class GlobalLoadingService
|
||||
{
|
||||
private bool _isLoading;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace AliasVault.Client.Services;
|
||||
/// are stored in this object which is scoped to the current session. This allows the messages to be cached until
|
||||
/// they actually have been displayed. So they can survive redirects and page reloads.
|
||||
/// </summary>
|
||||
public class GlobalNotificationService
|
||||
public sealed class GlobalNotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// Allow other components to subscribe to changes in the event object.
|
||||
@@ -20,14 +20,14 @@ public class GlobalNotificationService
|
||||
public event Action? OnChange;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets success messages that should be displayed to the user.
|
||||
/// Gets success messages that should be displayed to the user.
|
||||
/// </summary>
|
||||
protected List<string> SuccessMessages { get; set; } = [];
|
||||
private List<string> SuccessMessages { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets error messages that should be displayed to the user.
|
||||
/// Gets error messages that should be displayed to the user.
|
||||
/// </summary>
|
||||
protected List<string> ErrorMessages { get; set; } = [];
|
||||
private List<string> ErrorMessages { get; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a success message to the list of messages that should be displayed to the user.
|
||||
|
||||
@@ -15,7 +15,7 @@ using Microsoft.JSInterop;
|
||||
/// JavaScript interop service for calling JavaScript functions from C#.
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime">IJSRuntime.</param>
|
||||
public class JsInteropService(IJSRuntime jsRuntime)
|
||||
public sealed class JsInteropService(IJSRuntime jsRuntime)
|
||||
{
|
||||
/// <summary>
|
||||
/// Symmetrically encrypts a string using the provided encryption key.
|
||||
@@ -44,6 +44,22 @@ public class JsInteropService(IJSRuntime jsRuntime)
|
||||
public async Task DownloadFileFromStream(string filename, byte[] blob) =>
|
||||
await jsRuntime.InvokeVoidAsync("downloadFileFromStream", filename, blob);
|
||||
|
||||
/// <summary>
|
||||
/// Focus an element by its ID.
|
||||
/// </summary>
|
||||
/// <param name="elementId">The element ID to focus.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task FocusElementById(string elementId) =>
|
||||
await jsRuntime.InvokeVoidAsync("focusElement", elementId);
|
||||
|
||||
/// <summary>
|
||||
/// Blur (defocus) an element by its ID.
|
||||
/// </summary>
|
||||
/// <param name="elementId">The element ID to focus.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task BlurElementById(string elementId) =>
|
||||
await jsRuntime.InvokeVoidAsync("blurElement", elementId);
|
||||
|
||||
/// <summary>
|
||||
/// Copy a string to the browsers clipboard.
|
||||
/// </summary>
|
||||
|
||||
125
src/AliasVault.Client/Services/KeyboardShortcutService.cs
Normal file
125
src/AliasVault.Client/Services/KeyboardShortcutService.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="KeyboardShortcutService.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.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
/// Service class for alias operations.
|
||||
/// </summary>
|
||||
public sealed class KeyboardShortcutService : IAsyncDisposable
|
||||
{
|
||||
private readonly DotNetObjectReference<CallbackWrapper> _dotNetHelper;
|
||||
private readonly NavigationManager _navigationManager;
|
||||
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="KeyboardShortcutService"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsRuntime">IJSRuntime instance.</param>
|
||||
/// <param name="navigationManager">NavigationManager instance.</param>
|
||||
public KeyboardShortcutService(IJSRuntime jsRuntime, NavigationManager navigationManager)
|
||||
{
|
||||
_dotNetHelper = DotNetObjectReference.Create(new CallbackWrapper());
|
||||
_navigationManager = navigationManager;
|
||||
|
||||
moduleTask = new Lazy<Task<IJSObjectReference>>(() => jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||
"import", "./js/modules/keyboardShortcuts.js").AsTask());
|
||||
|
||||
_ = RegisterStaticShortcuts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a keyboard shortcut with the given keys and callback.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keyboard keys.</param>
|
||||
/// <param name="callback">Callback when shortcut is pressed.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task RegisterShortcutAsync(string keys, Func<Task> callback)
|
||||
{
|
||||
_dotNetHelper.Value.RegisterCallback(keys, callback);
|
||||
var module = await moduleTask.Value;
|
||||
await module.InvokeVoidAsync("registerShortcut", keys, _dotNetHelper);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a keyboard shortcut with the given keys.
|
||||
/// </summary>
|
||||
/// <param name="keys">The keyboard keys.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public async Task UnregisterShortcutAsync(string keys)
|
||||
{
|
||||
_dotNetHelper.Value.UnregisterCallback(keys);
|
||||
var module = await moduleTask.Value;
|
||||
await module!.InvokeVoidAsync("unregisterShortcut", keys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the service.
|
||||
/// </summary>
|
||||
/// <returns>ValueTask.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
var module = await moduleTask.Value;
|
||||
await module.InvokeVoidAsync("unregisterAllShortcuts");
|
||||
await module.DisposeAsync();
|
||||
|
||||
_dotNetHelper.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers static shortcuts that are always available.
|
||||
/// </summary>
|
||||
private async Task RegisterStaticShortcuts()
|
||||
{
|
||||
// Global shortcut: Go home
|
||||
await RegisterShortcutAsync("gh", () =>
|
||||
{
|
||||
_navigationManager.NavigateTo("/");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
// Global shortcut: Go to email page
|
||||
await RegisterShortcutAsync("ge", () =>
|
||||
{
|
||||
_navigationManager.NavigateTo("/emails");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper class for callback functions that are invoked from JavaScript.
|
||||
/// </summary>
|
||||
private sealed class CallbackWrapper
|
||||
{
|
||||
private readonly Dictionary<string, Func<Task>> _callbacks = new Dictionary<string, Func<Task>>();
|
||||
|
||||
public void RegisterCallback(string keys, Func<Task> callback)
|
||||
{
|
||||
_callbacks[keys] = callback;
|
||||
}
|
||||
|
||||
public void UnregisterCallback(string keys)
|
||||
{
|
||||
_callbacks.Remove(keys);
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public async Task Invoke(string keys)
|
||||
{
|
||||
if (_callbacks.TryGetValue(keys, out var callback))
|
||||
{
|
||||
await callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,14 +20,14 @@ using Microsoft.EntityFrameworkCore;
|
||||
/// This is done because the SettingsService requires a DbContext during initialization and the context is not yet
|
||||
/// available during application boot because of encryption/decryption of remote database file. When accessing the
|
||||
/// settings through the DbService we can ensure proper data flow.</remarks>
|
||||
public class SettingsService
|
||||
public sealed class SettingsService
|
||||
{
|
||||
private readonly Dictionary<string, string?> _settings = new();
|
||||
private DbService? _dbService;
|
||||
private bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the DefaultEmailDomain setting asynchronously.
|
||||
/// Gets the DefaultEmailDomain setting.
|
||||
/// </summary>
|
||||
/// <returns>Default email domain as string.</returns>
|
||||
public string DefaultEmailDomain => GetSetting("DefaultEmailDomain");
|
||||
@@ -39,19 +39,32 @@ public class SettingsService
|
||||
public bool AutoEmailRefresh => GetSetting<bool>("AutoEmailRefresh", true);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the DefaultEmailDomain setting asynchronously.
|
||||
/// Gets the DefaultIdentityLanguage setting.
|
||||
/// </summary>
|
||||
/// <param name="value">The new DeafultEmailDomain setting.</param>
|
||||
/// <returns>Default identity language as two-letter code.</returns>
|
||||
public string DefaultIdentityLanguage => GetSetting<string>("DefaultIdentityLanguage", "en")!;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the DefaultEmailDomain setting.
|
||||
/// </summary>
|
||||
/// <param name="value">The new DefaultEmailDomain setting.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SetDefaultEmailDomain(string value) => SetSettingAsync("DefaultEmailDomain", value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the AutoEmailRefresh setting asynchronously as a string.
|
||||
/// Sets the AutoEmailRefresh setting as a string.
|
||||
/// </summary>
|
||||
/// <param name="value">The new value.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SetAutoEmailRefresh(bool value) => SetSettingAsync<bool>("AutoEmailRefresh", value);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the DefaultIdentityLanguage setting.
|
||||
/// </summary>
|
||||
/// <param name="value">The new value.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SetDefaultIdentityLanguage(string value) => SetSettingAsync("DefaultIdentityLanguage", value);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the settings service asynchronously.
|
||||
/// </summary>
|
||||
@@ -171,11 +184,7 @@ public class SettingsService
|
||||
{
|
||||
string value = GetSetting(key);
|
||||
|
||||
try
|
||||
{
|
||||
return CastSetting<T>(value);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
// If no value is available in database but default value is set, return default value.
|
||||
if (defaultValue is not null)
|
||||
@@ -184,6 +193,15 @@ public class SettingsService
|
||||
}
|
||||
|
||||
// No value in database and no default value set, throw exception.
|
||||
throw new InvalidOperationException($"Setting {key} is not set and no default value is provided");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return CastSetting<T>(value);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to cast setting {key} to type {typeof(T)}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,12 @@
|
||||
@using AliasVault.Client.Main.Components.Forms
|
||||
@using AliasVault.Client.Main.Components.Layout
|
||||
@using AliasVault.Client.Main.Components.Loading
|
||||
@using AliasVault.Client.Main.Components.Widgets
|
||||
@using AliasVault.Client.Main.Models
|
||||
@using AliasVault.Client.Services
|
||||
@using AliasVault.Client.Services.Auth
|
||||
@using AliasVault.Client.Services.Database
|
||||
@using AliasVault.RazorComponents
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||
@using Blazored.LocalStorage
|
||||
|
||||
@@ -3,6 +3,7 @@ module.exports = {
|
||||
content: [
|
||||
'./**/*.html',
|
||||
'./**/*.razor',
|
||||
'../Utilities/AliasVault.RazorComponents/**/*.razor',
|
||||
],
|
||||
safelist: [
|
||||
'w-64',
|
||||
|
||||
@@ -1,41 +1,3 @@
|
||||
html, body {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: #0071c1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #1b6ec2;
|
||||
border-color: #1861ac;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
}
|
||||
|
||||
.valid.modified:not([type=checkbox]) {
|
||||
outline: 1px solid #26b050;
|
||||
}
|
||||
|
||||
.invalid {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
background: lightyellow;
|
||||
bottom: 0;
|
||||
@@ -48,12 +10,12 @@ a, .btn-link {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
#blazor-error-ui .dismiss {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: 0.75rem;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
@@ -61,43 +23,14 @@ a, .btn-link {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
margin: 20vh auto 1rem auto;
|
||||
.blazor-error-boundary::after {
|
||||
content: "An error has occurred."
|
||||
}
|
||||
|
||||
.loading-progress circle {
|
||||
fill: none;
|
||||
stroke: #e0e0e0;
|
||||
stroke-width: 0.6rem;
|
||||
transform-origin: 50% 50%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.loading-progress circle:last-child {
|
||||
stroke: #1b6ec2;
|
||||
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
|
||||
transition: stroke-dasharray 0.05s ease-in-out;
|
||||
}
|
||||
|
||||
.loading-progress-text {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
.loading-progress-text:after {
|
||||
content: var(--blazor-load-percentage-text, "Loading");
|
||||
}
|
||||
|
||||
code {
|
||||
color: #c02d76;
|
||||
.validation-message {
|
||||
color: red;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,12 +33,26 @@
|
||||
</head>
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-800">
|
||||
<div id="loading-screen">
|
||||
<div class="fixed inset-0 flex items-center justify-center">
|
||||
<div class="relative p-8 bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md mx-auto">
|
||||
<div class="text-center">
|
||||
<svg class="mx-auto animate-spin h-12 w-12 text-primary-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<h2 class="mt-4 text-xl font-semibold text-gray-900 dark:text-white">AliasVault is loading</h2>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Initializing secure environment. AliasVault prioritizes your privacy, which may take a few seconds.</p>
|
||||
<div class="loading-progress-text text-sm font-medium text-gray-700 mt-4"></div>
|
||||
<div class="mt-6 text-center">
|
||||
<p id="security-quote" class="text-sm text-primary-600 italic"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<svg class="loading-progress">
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
<circle r="40%" cx="50%" cy="50%" />
|
||||
</svg>
|
||||
<div class="loading-progress-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
@@ -47,8 +61,48 @@
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="js/dark-mode.js?v=@CacheBuster"></script>
|
||||
<script src="js/cryptoInterop.js?v=@CacheBuster"></script>
|
||||
<script>
|
||||
const securityQuotes = [
|
||||
"Your identity is your most valuable asset. Protect it like one.",
|
||||
"In the digital world, a strong password is your first line of defense.",
|
||||
"Security is not a product, but a process.",
|
||||
"The weakest link in the security chain is the human element.",
|
||||
"Security is always excessive until it's not enough.",
|
||||
"Trust, but verify - especially online.",
|
||||
"Your data is only as secure as your weakest password.",
|
||||
"The most secure password is the one you can't remember.",
|
||||
];
|
||||
|
||||
const quoteElement = document.getElementById('security-quote');
|
||||
const randomIndex = Math.floor(Math.random() * securityQuotes.length);
|
||||
quoteElement.textContent = `"${securityQuotes[randomIndex]}"`;
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
const startTime = new Date().getTime();
|
||||
const minDisplayTime = 1000;
|
||||
|
||||
function hideLoadingScreen() {
|
||||
document.getElementById('loading-screen').style.display = 'none';
|
||||
document.getElementById('app').style.removeProperty('visibility');
|
||||
}
|
||||
|
||||
function checkElapsedTime() {
|
||||
const currentTime = new Date().getTime();
|
||||
const elapsedTime = currentTime - startTime;
|
||||
|
||||
if (elapsedTime >= minDisplayTime) {
|
||||
hideLoadingScreen();
|
||||
} else {
|
||||
setTimeout(hideLoadingScreen, minDisplayTime - elapsedTime);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('app').style.visibility = 'hidden';
|
||||
document.getElementById('loading-screen').style.display = 'flex';
|
||||
checkElapsedTime();
|
||||
});
|
||||
</script>
|
||||
<script src="js/crypto.js?v=@CacheBuster"></script>
|
||||
<script src="js/utilities.js?v=@CacheBuster"></script>
|
||||
<script src="_framework/blazor.webassembly.js?v=@CacheBuster"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
function initDarkModeSwitcher() {
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggleDarkIcon === null && themeToggleLightIcon === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to light mode if not set.
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
let event = new Event('dark-mode');
|
||||
|
||||
themeToggleBtn.addEventListener('click', function () {
|
||||
// toggle icons
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
|
||||
// if set via local storage previously
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
}
|
||||
// if NOT set via local storage previously
|
||||
} else if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
// clickOutsideHandler.js
|
||||
let currentHandler = null;
|
||||
let currentDotNetHelper = null;
|
||||
|
||||
export function registerClickOutsideHandler(dotNetHelper, contentId, methodName) {
|
||||
export function registerClickOutsideHandler(dotNetHelper, contentIds, methodName) {
|
||||
unregisterClickOutsideHandler();
|
||||
|
||||
currentDotNetHelper = dotNetHelper;
|
||||
currentHandler = (event) => {
|
||||
const content = document.getElementById(contentId);
|
||||
if (!content) return;
|
||||
const idArray = Array.isArray(contentIds) ? contentIds : contentIds.split(',').map(id => id.trim());
|
||||
|
||||
currentHandler = (event) => {
|
||||
const isOutside = idArray.every(id => {
|
||||
const content = document.getElementById(id);
|
||||
return !content?.contains(event.target);
|
||||
});
|
||||
|
||||
const isOutside = !content.contains(event.target);
|
||||
const isEscapeKey = event.type === 'keydown' && event.key === 'Escape';
|
||||
|
||||
if (isOutside || isEscapeKey) {
|
||||
@@ -0,0 +1,40 @@
|
||||
let shortcuts = {};
|
||||
let lastKeyPressed = '';
|
||||
let lastKeyPressTime = 0;
|
||||
|
||||
document.addEventListener('keydown', handleKeyPress);
|
||||
|
||||
export function handleKeyPress(event) {
|
||||
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTime = new Date().getTime();
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
if (currentTime - lastKeyPressTime > 1000) {
|
||||
lastKeyPressed = '';
|
||||
}
|
||||
|
||||
lastKeyPressed += key;
|
||||
lastKeyPressTime = currentTime;
|
||||
|
||||
const shortcut = shortcuts[lastKeyPressed];
|
||||
if (shortcut) {
|
||||
event.preventDefault();
|
||||
shortcut.dotNetHelper.invokeMethodAsync('Invoke', lastKeyPressed);
|
||||
lastKeyPressed = '';
|
||||
}
|
||||
}
|
||||
|
||||
export function registerShortcut(keys, dotNetHelper) {
|
||||
shortcuts[keys.toLowerCase()] = { dotNetHelper: dotNetHelper };
|
||||
}
|
||||
|
||||
export function unregisterShortcut(keys) {
|
||||
delete shortcuts[keys.toLowerCase()];
|
||||
}
|
||||
|
||||
export function unregisterAllShortcuts() {
|
||||
shortcuts = {};
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
window.getWindowWidth = function() {
|
||||
return window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||
};
|
||||
window.getElementRect = function(element) {
|
||||
if (element) {
|
||||
const rect = {
|
||||
left: element.offsetLeft,
|
||||
top: element.offsetTop,
|
||||
right: element.offsetLeft + element.offsetWidth,
|
||||
bottom: element.offsetTop + element.offsetHeight,
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight
|
||||
};
|
||||
let parent = element.offsetParent;
|
||||
while (parent) {
|
||||
rect.left += parent.offsetLeft;
|
||||
rect.top += parent.offsetTop;
|
||||
parent = parent.offsetParent;
|
||||
}
|
||||
rect.right = rect.left + rect.width;
|
||||
rect.bottom = rect.top + rect.height;
|
||||
return rect;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -10,10 +10,6 @@ function downloadFileFromStream(fileName, contentStreamReference) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
window.topMenuClickOutsideHandler = (dotNetHelper) => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const menu = document.getElementById('userMenuDropdown');
|
||||
@@ -43,3 +39,76 @@ window.clipboardCopy = {
|
||||
window.blazorNavigate = (url) => {
|
||||
Blazor.navigateTo(url);
|
||||
};
|
||||
|
||||
window.focusElement = (elementId) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
window.blurElement = (elementId) => {
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.blur();
|
||||
}
|
||||
};
|
||||
|
||||
function initDarkModeSwitcher() {
|
||||
const themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon');
|
||||
const themeToggleLightIcon = document.getElementById('theme-toggle-light-icon');
|
||||
|
||||
if (themeToggleDarkIcon === null && themeToggleLightIcon === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
themeToggleDarkIcon.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Default to light mode if not set.
|
||||
document.documentElement.classList.remove('dark');
|
||||
themeToggleLightIcon.classList.remove('hidden');
|
||||
}
|
||||
|
||||
const themeToggleBtn = document.getElementById('theme-toggle');
|
||||
|
||||
let event = new Event('dark-mode');
|
||||
|
||||
themeToggleBtn.addEventListener('click', function () {
|
||||
// toggle icons
|
||||
themeToggleDarkIcon.classList.toggle('hidden');
|
||||
themeToggleLightIcon.classList.toggle('hidden');
|
||||
|
||||
// if set via local storage previously
|
||||
if (localStorage.getItem('color-theme')) {
|
||||
if (localStorage.getItem('color-theme') === 'light') {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
}
|
||||
// if NOT set via local storage previously
|
||||
} else if (document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
localStorage.setItem('color-theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
localStorage.setItem('color-theme', 'dark');
|
||||
}
|
||||
|
||||
document.dispatchEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
window.initTopMenu = function() {
|
||||
initDarkModeSwitcher();
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MailboxBulkRequest.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.Shared.Models.WebApi.Email;
|
||||
|
||||
/// <summary>
|
||||
/// MailboxBulkRequest model for retrieving recent emails from multiple emailboxes.
|
||||
/// </summary>
|
||||
public class MailboxBulkRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the mailbox addresses that client wants to retrieve emails for.
|
||||
/// </summary>
|
||||
public List<string> Addresses { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets requested page number.
|
||||
/// </summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets requested page size.
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MailboxBulkResponse.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.Shared.Models.WebApi.Email;
|
||||
|
||||
using AliasVault.Shared.Models.Spamok;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a mailbox API model.
|
||||
/// </summary>
|
||||
public class MailboxBulkResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the mailbox addresses that client wants to retrieve emails for.
|
||||
/// </summary>
|
||||
public List<string> Addresses { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets requested page number.
|
||||
/// </summary>
|
||||
public int CurrentPage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets requested page size.
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets total number of emails.
|
||||
/// </summary>
|
||||
public int TotalRecords { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of mailbox email API models.
|
||||
/// </summary>
|
||||
public List<MailboxEmailApiModel> Mails { get; set; } = [];
|
||||
}
|
||||
@@ -53,65 +53,12 @@ public class Alias
|
||||
/// </summary>
|
||||
public DateTime BirthDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address street.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressStreet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address city.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressCity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address state.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address zip code.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressZipCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the address country.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
[Column(TypeName = "VARCHAR")]
|
||||
public string? AddressCountry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hobbies in CSV format, can contain multiple values separated by ";".
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? Hobbies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated email.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the random generated mobile phone number.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? PhoneMobile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the generated IBAN bank account number.
|
||||
/// </summary>
|
||||
[StringLength(255)]
|
||||
public string? BankAccountIBAN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the created timestamp.
|
||||
/// </summary>
|
||||
|
||||
@@ -39,7 +39,7 @@ public class Credential
|
||||
/// <summary>
|
||||
/// Gets or sets the username field.
|
||||
/// </summary>
|
||||
public string Username { get; set; } = null!;
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password objects.
|
||||
|
||||
296
src/Databases/AliasClientDb/Migrations/20240805122422_1.3.0-UpdateIdentityStructure.Designer.cs
generated
Normal file
296
src/Databases/AliasClientDb/Migrations/20240805122422_1.3.0-UpdateIdentityStructure.Designer.cs
generated
Normal file
@@ -0,0 +1,296 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasClientDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasClientDbContext))]
|
||||
[Migration("20240805122422_1.3.0-UpdateIdentityStructure")]
|
||||
partial class _130UpdateIdentityStructure
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Attachment");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AliasId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ServiceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AliasId");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.ToTable("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.EncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrivateKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("EncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Logo")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Alias", "Alias")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("AliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasClientDb.Service", "Service")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alias");
|
||||
|
||||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Passwords")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class _130UpdateIdentityStructure : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AddressCity",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AddressCountry",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AddressState",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AddressStreet",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AddressZipCode",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "BankAccountIBAN",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Hobbies",
|
||||
table: "Aliases");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PhoneMobile",
|
||||
table: "Aliases");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AddressCity",
|
||||
table: "Aliases",
|
||||
type: "VARCHAR",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AddressCountry",
|
||||
table: "Aliases",
|
||||
type: "VARCHAR",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AddressState",
|
||||
table: "Aliases",
|
||||
type: "VARCHAR",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AddressStreet",
|
||||
table: "Aliases",
|
||||
type: "VARCHAR",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "AddressZipCode",
|
||||
table: "Aliases",
|
||||
type: "VARCHAR",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "BankAccountIBAN",
|
||||
table: "Aliases",
|
||||
type: "TEXT",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Hobbies",
|
||||
table: "Aliases",
|
||||
type: "TEXT",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PhoneMobile",
|
||||
table: "Aliases",
|
||||
type: "TEXT",
|
||||
maxLength: 255,
|
||||
nullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
295
src/Databases/AliasClientDb/Migrations/20240812141727_1.3.1-MakeUsernameOptional.Designer.cs
generated
Normal file
295
src/Databases/AliasClientDb/Migrations/20240812141727_1.3.1-MakeUsernameOptional.Designer.cs
generated
Normal file
@@ -0,0 +1,295 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using AliasClientDb;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
[DbContext(typeof(AliasClientDbContext))]
|
||||
[Migration("20240812141727_1.3.1-MakeUsernameOptional")]
|
||||
partial class _131MakeUsernameOptional
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.7")
|
||||
.HasAnnotation("Proxies:ChangeTracking", false)
|
||||
.HasAnnotation("Proxies:CheckEquality", false)
|
||||
.HasAnnotation("Proxies:LazyLoading", true);
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("NickName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Aliases");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Blob")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Filename")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Attachment");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("AliasId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Notes")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("ServiceId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AliasId");
|
||||
|
||||
b.HasIndex("ServiceId");
|
||||
|
||||
b.ToTable("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.EncryptionKey", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("PrivateKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PublicKey")
|
||||
.IsRequired()
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("EncryptionKeys");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CredentialId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CredentialId");
|
||||
|
||||
b.ToTable("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<byte[]>("Logo")
|
||||
.HasColumnType("BLOB");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Services");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Key")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Key");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Attachment", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Attachments")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Alias", "Alias")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("AliasId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("AliasClientDb.Service", "Service")
|
||||
.WithMany("Credentials")
|
||||
.HasForeignKey("ServiceId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Alias");
|
||||
|
||||
b.Navigation("Service");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Password", b =>
|
||||
{
|
||||
b.HasOne("AliasClientDb.Credential", "Credential")
|
||||
.WithMany("Passwords")
|
||||
.HasForeignKey("CredentialId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Credential");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Alias", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Credential", b =>
|
||||
{
|
||||
b.Navigation("Attachments");
|
||||
|
||||
b.Navigation("Passwords");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AliasClientDb.Service", b =>
|
||||
{
|
||||
b.Navigation("Credentials");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// <auto-generated />
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace AliasClientDb.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class _131MakeUsernameOptional : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Username",
|
||||
table: "Credentials",
|
||||
type: "TEXT",
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Username",
|
||||
table: "Credentials",
|
||||
type: "TEXT",
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "TEXT",
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,30 +27,6 @@ namespace AliasClientDb.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("AddressCity")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressCountry")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressState")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressStreet")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("AddressZipCode")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("BankAccountIBAN")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("BirthDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -69,10 +45,6 @@ namespace AliasClientDb.Migrations
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("Hobbies")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
@@ -81,10 +53,6 @@ namespace AliasClientDb.Migrations
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("VARCHAR");
|
||||
|
||||
b.Property<string>("PhoneMobile")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
@@ -146,7 +114,6 @@ namespace AliasClientDb.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
16
src/Services/AliasVault.SmtpService/Scripts/README.md
Normal file
16
src/Services/AliasVault.SmtpService/Scripts/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
This directory contains various scripts for the SMTP service.
|
||||
|
||||
In order to use the scripts you will need to give execute permissions to the scripts. You can do this by running the following command in the terminal:
|
||||
|
||||
```bash
|
||||
$ chmod +x sendEmailCLI.sh
|
||||
```
|
||||
|
||||
Then you can run the script by running the following command in the terminal:
|
||||
|
||||
```bash
|
||||
$ ./sendEmailCLI.sh
|
||||
```
|
||||
|
||||
## Scripts
|
||||
- `sendEmailCLI.sh`: A script to send an email from the command line to the local SMTP server for testing purposes.
|
||||
@@ -1 +0,0 @@
|
||||
curl --url "smtp://localhost:25" --mail-from "sender@example.com" --mail-rcpt "test@example.tld" --upload-file testEmail1.txt
|
||||
67
src/Services/AliasVault.SmtpService/Scripts/sendEmailCLI.sh
Executable file
67
src/Services/AliasVault.SmtpService/Scripts/sendEmailCLI.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
generate_random_string() {
|
||||
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-10} | head -n 1
|
||||
}
|
||||
|
||||
print_logo() {
|
||||
printf "${MAGENTA}\n"
|
||||
printf "=========================================================\n"
|
||||
printf " _ _ __ __ _ _ \n"
|
||||
printf " /\ | (_) \ \ / / | | | \n"
|
||||
printf " / \ | |_ __ _ __\ \ / /_ _ _ _| | |_\n"
|
||||
printf " / /\ \ | | |/ _ / __\ \/ / _ | | | | | __|\n"
|
||||
printf " / ____ \| | | (_| \__ \\ / (_| | |_| | | |_ \n"
|
||||
printf " /_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf "\n"
|
||||
printf " Email sender DevTool\n"
|
||||
printf "=========================================================\n"
|
||||
printf "This tool sends an email to the recipient of your choice\n"
|
||||
printf "and delivers it to the local SMTP server running on localhost:25.\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
send_email() {
|
||||
local recipient="$1"
|
||||
local subject_suffix=$(generate_random_string 8)
|
||||
local content_suffix=$(generate_random_string 20)
|
||||
|
||||
cat > temp_email.txt << EOF
|
||||
From: sender@example.com
|
||||
To: $recipient
|
||||
Subject: Test Email - $subject_suffix
|
||||
|
||||
This is a test email.
|
||||
|
||||
Random content: $content_suffix
|
||||
EOF
|
||||
|
||||
curl --url "smtp://localhost:25" \
|
||||
--mail-from "sender@example.com" \
|
||||
--mail-rcpt "$recipient" \
|
||||
--upload-file temp_email.txt
|
||||
|
||||
rm temp_email.txt
|
||||
}
|
||||
|
||||
print_logo
|
||||
|
||||
while true; do
|
||||
if [[ -z "$recipient" ]]; then
|
||||
read -p "Enter the recipient's email address: " recipient
|
||||
fi
|
||||
|
||||
send_email "$recipient"
|
||||
|
||||
read -p "Send another email? (Press Enter for same recipient, or type a new email, or 'q' to quit): " next_action
|
||||
|
||||
if [[ "$next_action" == "q" ]]; then
|
||||
echo "Exiting the script. Goodbye!"
|
||||
exit 0
|
||||
elif [[ -n "$next_action" ]]; then
|
||||
recipient="$next_action"
|
||||
else
|
||||
# If next_action is empty (user pressed Enter), keep the same recipient
|
||||
:
|
||||
fi
|
||||
done
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user