mirror of
https://github.com/aliasvault/aliasvault.git
synced 2025-12-29 09:09:59 -05:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81a52dcc0e | ||
|
|
dd3818499c | ||
|
|
b141196384 | ||
|
|
2dfe8c64e5 | ||
|
|
033a513c92 | ||
|
|
0bbb504511 | ||
|
|
88fe86c19a | ||
|
|
3ccf239d84 | ||
|
|
c6ef654c87 | ||
|
|
4ef0373d31 | ||
|
|
71f91b8050 | ||
|
|
6d8315ac4e | ||
|
|
08e550f46f | ||
|
|
dfa67a00e3 | ||
|
|
cdd58773de | ||
|
|
808ecb865b | ||
|
|
277ff6c012 | ||
|
|
836f7a311e | ||
|
|
a3b232543f | ||
|
|
6077f8f377 | ||
|
|
62711f603b | ||
|
|
a59fa22fa6 | ||
|
|
cc10bcdfb8 | ||
|
|
481c283c36 | ||
|
|
947988c2ce | ||
|
|
202df3d9c3 | ||
|
|
8f3ad3d171 | ||
|
|
aa771ae1b2 | ||
|
|
158a526aee | ||
|
|
8c4e078490 | ||
|
|
98dea2c4bf | ||
|
|
db62eeec22 | ||
|
|
af1e813c48 | ||
|
|
d57ac9e743 | ||
|
|
f5e02fb784 | ||
|
|
96a7fbaf3b | ||
|
|
c749853870 | ||
|
|
8c93fceb3e | ||
|
|
fb0ef1c59a | ||
|
|
847d97a0e9 | ||
|
|
8d9c80ef61 | ||
|
|
c22ba0c2cf | ||
|
|
5e4654a968 | ||
|
|
bbf08d16d4 | ||
|
|
19c6296a86 | ||
|
|
b8ffd39f99 | ||
|
|
fe29cb7a2c | ||
|
|
4137cc4736 | ||
|
|
3d23731f0e | ||
|
|
bcbda92601 | ||
|
|
f0fca573fd | ||
|
|
4abc674970 | ||
|
|
43ed35a1be | ||
|
|
689ab0b388 | ||
|
|
a884895fae | ||
|
|
a644df1e3c | ||
|
|
48abe09415 | ||
|
|
f1bc79a9a4 | ||
|
|
ce61fc36e1 | ||
|
|
cbce527aa1 | ||
|
|
53ea7c2477 | ||
|
|
aae0846639 | ||
|
|
c2648cf2cb | ||
|
|
68c19957e0 | ||
|
|
6c79503d1f | ||
|
|
c5fc5c0e81 | ||
|
|
4170e754ea | ||
|
|
5d44a3aeff | ||
|
|
8243213028 | ||
|
|
efc422ac44 | ||
|
|
497430f729 | ||
|
|
659dc7b55d | ||
|
|
581fd945c2 | ||
|
|
8205fa9d6e | ||
|
|
cc4a6db976 | ||
|
|
efb7ae009d | ||
|
|
f29606ea94 | ||
|
|
4a586cf117 | ||
|
|
64688cd2b5 | ||
|
|
ea9c6e9aa7 | ||
|
|
889dc81404 | ||
|
|
df85846fbe | ||
|
|
5a9632f80e | ||
|
|
9105215dc8 | ||
|
|
4aa57f95b5 | ||
|
|
1112922731 | ||
|
|
2bee131ff4 | ||
|
|
75933efbdd | ||
|
|
93623a2f05 | ||
|
|
4b12518ee4 | ||
|
|
2a834eeb38 | ||
|
|
25872f08de | ||
|
|
9fc0f4d7da | ||
|
|
4e2bf10115 | ||
|
|
957a9474ec | ||
|
|
bd0d4ad2a4 | ||
|
|
88fa8a0c17 | ||
|
|
943f16789f | ||
|
|
bb91637db5 | ||
|
|
5aef4c58e2 | ||
|
|
554ea91bda | ||
|
|
d5a858d78d | ||
|
|
ff265c3a86 | ||
|
|
f2fced86b2 | ||
|
|
83f62e17b2 | ||
|
|
0f80217e74 | ||
|
|
7290ee870c | ||
|
|
27c0c9194e | ||
|
|
ae652297fa | ||
|
|
bcdb9efee8 | ||
|
|
f224507f91 | ||
|
|
dc9ba64b21 | ||
|
|
c99394416e | ||
|
|
ba2abde97d | ||
|
|
66b33a8686 | ||
|
|
1ebdce5216 | ||
|
|
c2d1ea9895 | ||
|
|
75a9278d56 | ||
|
|
b508354ac6 | ||
|
|
e1478c055f | ||
|
|
c07f0c33bb | ||
|
|
4e2b10eeab | ||
|
|
05e9285752 | ||
|
|
a76a21a935 | ||
|
|
0fd23eab59 | ||
|
|
1c1d1e1d74 | ||
|
|
9412f862eb | ||
|
|
9f7ba2eb20 | ||
|
|
101d1d485a | ||
|
|
e316836ee5 | ||
|
|
c6e3c41759 | ||
|
|
bca7b2bc82 | ||
|
|
d51ae8d913 | ||
|
|
c5d2b1da37 | ||
|
|
5d85b3a275 | ||
|
|
3ba8e54e56 | ||
|
|
7ffc1f1ee5 | ||
|
|
8d4024860b | ||
|
|
383145814a | ||
|
|
210f4b3c9e | ||
|
|
276ceb3dce | ||
|
|
2985c8333e | ||
|
|
7bb8aee532 | ||
|
|
7de3b05985 | ||
|
|
daca01a428 | ||
|
|
9fb19d28d6 | ||
|
|
540177c762 | ||
|
|
228b037a6d | ||
|
|
0e0366564d | ||
|
|
cca91d6076 | ||
|
|
6c9e770af7 | ||
|
|
44bcb7f16d | ||
|
|
d69b3defe5 | ||
|
|
02af26cb39 | ||
|
|
3cc3c67a4d | ||
|
|
107d2d8602 | ||
|
|
b8301d8f98 | ||
|
|
124491e5db | ||
|
|
dbea1c2c4d | ||
|
|
949a7a856a | ||
|
|
b923669b66 | ||
|
|
da25aa43ea |
@@ -10,10 +10,7 @@ insert_final_newline = true
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
@@ -28,12 +25,6 @@ csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
dotnet_diagnostic.SA1011.severity = none
|
||||
dotnet_diagnostic.SA1101.severity = none
|
||||
dotnet_diagnostic.SA1200.severity = none
|
||||
dotnet_diagnostic.SA1309.severity = none
|
||||
dotnet_diagnostic.SA1310.severity = warning
|
||||
dotnet_diagnostic.SX1309.severity = none
|
||||
|
||||
# Razor files
|
||||
[*.razor]
|
||||
@@ -69,56 +60,3 @@ indent_size = 4
|
||||
[*.xml]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
|
||||
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
||||
JWT_KEY=
|
||||
8
.github/workflows/docker-compose-build.yml
vendored
8
.github/workflows/docker-compose-build.yml
vendored
@@ -15,6 +15,10 @@ jobs:
|
||||
options: --privileged
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set permissions and run init.sh
|
||||
run: |
|
||||
chmod +x init.sh
|
||||
./init.sh
|
||||
- name: Set up Docker Compose
|
||||
run: |
|
||||
# Build the images and start the services
|
||||
@@ -37,8 +41,8 @@ jobs:
|
||||
run: |
|
||||
# Test if the service on localhost:81 responds
|
||||
http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:81)
|
||||
if [ "$http_code" -ne 200 ] && [ "$http_code" -ne 404 ]; then
|
||||
echo "Service did not respond with expected 200 OK or 404 Not Found"
|
||||
if [ "$http_code" -ne 200 ]; then
|
||||
echo "Service did not respond with expected 200 OK. Check if all DB migrations are applied."
|
||||
exit 1
|
||||
else
|
||||
echo "Service responded with $http_code"
|
||||
|
||||
2
.github/workflows/dotnet-build-run-tests.yml
vendored
2
.github/workflows/dotnet-build-run-tests.yml
vendored
@@ -20,6 +20,8 @@ jobs:
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -19,6 +19,8 @@ jobs:
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet workload install wasm-tools
|
||||
- name: Build
|
||||
run: dotnet build
|
||||
- name: Ensure browsers are installed
|
||||
|
||||
49
.github/workflows/sonarcloud-code-analysis.yml
vendored
Normal file
49
.github/workflows/sonarcloud-code-analysis.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: SonarCloud code analysis
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
build:
|
||||
name: Build and analyze
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu' # Alternative distribution options are available.
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: Cache SonarCloud packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~\sonar\cache
|
||||
key: ${{ runner.os }}-sonar
|
||||
restore-keys: ${{ runner.os }}-sonar
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
- name: Install SonarCloud scanner
|
||||
if: steps.cache-sonar-scanner.outputs.cache-hit != 'true'
|
||||
shell: powershell
|
||||
run: |
|
||||
New-Item -Path .\.sonar\scanner -ItemType Directory
|
||||
dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner
|
||||
- name: Build and analyze
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
.\.sonar\scanner\dotnet-sonarscanner begin /k:"lanedirt_AliasVault" /o:"lanedirt" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="**/coverage.opencover.xml" /d:sonar.coverage.exclusions="**Tests*.cs"
|
||||
dotnet build
|
||||
dotnet test -c Release /p:CollectCoverage=true /p:CoverletOutput=coverage /p:CoverletOutputFormat=opencover --filter 'FullyQualifiedName!~AliasVault.E2ETests'
|
||||
.\.sonar\scanner\dotnet-sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -370,3 +370,14 @@ FodyWeavers.xsd
|
||||
|
||||
.idea
|
||||
*.licenseheader
|
||||
|
||||
# AliasVault specific
|
||||
# index.html is generated by the build process from index.template.html and therefore should be ignored
|
||||
src/AliasVault.WebApp/wwwroot/index.html
|
||||
# appsettings.Development.json is generated by the build process from appsettings.Development.template.json and therefore should be ignored
|
||||
src/AliasVault.WebApp/wwwroot/appsettings.Development.json
|
||||
# appsettings.Development.json is added manually if needed, it should not be committed.
|
||||
src/Tests/AliasVault.E2ETests/appsettings.Development.json
|
||||
|
||||
# .env is generated by init.sh and therefore should be ignored
|
||||
.env
|
||||
|
||||
57
.globalconfig
Normal file
57
.globalconfig
Normal file
@@ -0,0 +1,57 @@
|
||||
dotnet_diagnostic.SA1011.severity = none
|
||||
dotnet_diagnostic.SA1101.severity = none
|
||||
dotnet_diagnostic.SA1309.severity = none
|
||||
dotnet_diagnostic.SA1310.severity = warning
|
||||
dotnet_diagnostic.SX1309.severity = none
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
|
||||
dotnet_style_coalesce_expression = false:suggestion
|
||||
dotnet_style_null_propagation = false:suggestion
|
||||
|
||||
# IDE0046: Convert to conditional expression
|
||||
dotnet_diagnostic.IDE0046.severity = silent
|
||||
19
.vscode/launch.json
vendored
Normal file
19
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "C#: AliasVault.WebApp [http]",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.WebApp/AliasVault.WebApp.csproj",
|
||||
"launchConfigurationId": "TargetFramework=;http"
|
||||
},
|
||||
{
|
||||
"name": "C#: AliasVault.Api [http]",
|
||||
"type": "dotnet",
|
||||
"request": "launch",
|
||||
"projectPath": "${workspaceFolder}/src/AliasVault.Api/AliasVault.Api.csproj",
|
||||
"launchConfigurationId": "TargetFramework=;http"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -68,3 +68,21 @@ dotnet tool install --global Microsoft.Playwright.CLI
|
||||
# Note: make sure the E2E test project has been built at least once so the bin dir exists.
|
||||
pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net8.0/playwright.ps1 install
|
||||
```
|
||||
|
||||
### 7. Create AliasVault.WebApp appsettings.Development.json
|
||||
The WASM app supports a development specific appsettings.json file. This appsettings file is optional but can override various options to make debugging easier.
|
||||
|
||||
|
||||
1. Copy `wwwroot/appsettings.json` to `wwwroot/appsettings.Development.json`
|
||||
|
||||
Here is an example file with the various options explained:
|
||||
|
||||
```
|
||||
{
|
||||
"ApiUrl": "http://localhost:5092",
|
||||
"UseDebugEncryptionKey": "true"
|
||||
}
|
||||
```
|
||||
|
||||
- UseDebugEncryptionKey
|
||||
- This setting will use a static encryption key so that if you login as a user you can refresh the page without needing to unlock the database again. This speeds up development when changing things in the WebApp WASM project. Note: the project needs to be run in "Development" mode for this setting to be used.
|
||||
|
||||
13
ENCRYPTION.md
Normal file
13
ENCRYPTION.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Encryption
|
||||
This document describes the encryption used in AliasVault.
|
||||
|
||||
## SRP
|
||||
The application uses the Secure Remote Password (SRP) protocol for authentication. The SRP protocol is a password-authenticated key agreement protocol. This means that the client and server can authenticate each other using a password, without sending the password over the network.
|
||||
|
||||
With the use of SRP the master password never leaves the client. The client sends a verifier to the server, which is a value derived from the master password. The server uses this verifier to authenticate the client. With this the server can authenticate the client without having ever seen the actual master password.
|
||||
|
||||
## Argon2id
|
||||
The application uses the Argon2id key derivation function to derive a key from the master password. Argon2id is a memory-hard function, which makes it difficult to perform large-scale custom hardware attacks. This makes it a good choice for password hashing.
|
||||
|
||||
## AES
|
||||
AES-256 IV is used to encrypt the data. The data is encrypted with a key derived from the master password using Argon2id. The Initialization Vector (IV) is generated randomly for each encryption.
|
||||
82
README.md
82
README.md
@@ -1,47 +1,83 @@
|
||||
<div align="center">
|
||||
|
||||
<h1>AliasVault</h1>
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/OGameX/releases)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-build-run-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-integration-tests.yml)
|
||||
|
||||
[<img src="https://img.shields.io/github/v/release/lanedirt/AliasVault?include_prereleases&logo=github">](https://github.com/lanedirt/OGameX/releases)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/docker-compose-build.yml?label=docker-compose%20build">](https://github.com/lanedirt/AliasVault/actions/workflows/docker-compose-build.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-build-run-tests.yml?label=unit tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-build-run-tests.yml)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/lanedirt/AliasVault/dotnet-integration-tests.yml?label=e2e tests">](https://github.com/lanedirt/AliasVault/actions/workflows/dotnet-integration-tests.yml)
|
||||
[<img src="https://img.shields.io/sonar/coverage/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=test code coverage">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
[<img src="https://img.shields.io/sonar/quality_gate/lanedirt_AliasVault?server=https%3A%2F%2Fsonarcloud.io&label=sonarcloud&logo=sonarcloud">](https://sonarcloud.io/summary/new_code?id=lanedirt_AliasVault)
|
||||
</div>
|
||||
|
||||
> Disclaimer: This repository is currently in an alpha state and is NOT ready for production use. Critical features, such as encryption, are not yet fully implemented. AliasVault is a work in progress and as of this moment serves as a research playground. Users are welcome to explore and use this project, but please be aware that there are no guarantees regarding its security or stability. Use at your own risk!
|
||||
AliasVault is an open-source password and identity manager built with C# ASP.NET technology. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
|
||||
|
||||
AliasVault is an open-source password manager that can generate virtual identities complete with virtual email addresses. AliasVault can be self-hosted on your own server with Docker, providing a secure and private solution for managing your online identities and passwords.
|
||||
### What makes AliasVault unique:
|
||||
- **Zero-knowledge architecture**: All data is end-to-end encrypted on the client and stored in encrypted state on the server. Your master password never leaves your device and the server never has access to your data.
|
||||
- **Virtual identities**: Generate virtual identities with virtual (working) email addresses that are assigned to one or more passwords.
|
||||
- **Open-source**: The source code is available on GitHub and can be self-hosted on your own server.
|
||||
|
||||
## Features
|
||||
- Password Management: Securely store and manage your passwords.
|
||||
- Virtual Identities: Generate virtual identities with virtual (working) email addresses.
|
||||
- Data Protection: Ensures that all sensitive data is encrypted and securely stored.
|
||||
- User Authentication: Secure login and user management functionalities.
|
||||
> Note: AliasVault is currently in development and not yet ready for production use. The project is still in the early stages and many features are not yet implemented. You are welcome to contribute to the project by submitting pull requests or opening issues.
|
||||
|
||||
## Installation
|
||||
## Live demo
|
||||
A live demo of the app is available at [main.aliasvault.net](https://main.aliasvault.net) (nightly builds). You can create a free account to try it out yourself.
|
||||
|
||||
1. Clone this repository.
|
||||
<img width="700" alt="Screenshot 2024-07-12 at 14 58 29" src="https://github.com/user-attachments/assets/57103f67-dff0-4124-9b33-62137aab5578">
|
||||
|
||||
## Installation on your own machine
|
||||
To install AliasVault on your own machine, follow the steps below. Note: the install process is tested on MacOS and Linux. It should work on Windows too, but you might need to adjust some commands.
|
||||
|
||||
### Requirements:
|
||||
- Access to a terminal
|
||||
- Docker
|
||||
- Git
|
||||
|
||||
### 1. Clone this repository.
|
||||
|
||||
```bash
|
||||
git clone [URL]
|
||||
# Clone this Git repository to "AliasVault" directory
|
||||
$ git clone https://github.com/lanedirt/AliasVault.git
|
||||
```
|
||||
|
||||
2. Run the app via Docker:
|
||||
### 2. Run the init script.
|
||||
This script will create a .env file in the root directory of the project if it does not yet exist and populate it with a random encryption secret.
|
||||
```bash
|
||||
# Go to the project directory
|
||||
$ cd AliasVault
|
||||
|
||||
# Make init script executable
|
||||
$ chmod +x init.sh
|
||||
|
||||
# Run the init script
|
||||
$ ./init.sh
|
||||
```
|
||||
|
||||
### 3. Build and run the app via Docker:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build --force-recreate
|
||||
# Build and run the app via Docker Compose
|
||||
$ docker compose up -d --build --force-recreate
|
||||
```
|
||||
> Note: the container binds to port 80 by default. If you have another service running on port 80, you can change the port in the `docker-compose.yml` file.
|
||||
|
||||
The app will be available at http://localhost:80
|
||||
#### Note for first time build:
|
||||
- When running the docker compose command for the first time, it may take a few minutes to build the Docker image.
|
||||
- A SQLite database file will be created in `./database/AliasServerDb.sqlite`. This file will store all (encrypted) password vaults. It should be kept secure and not shared.
|
||||
|
||||
After the Docker containers have started the app will be available at http://localhost:80
|
||||
|
||||
## Tech stack / credits
|
||||
The following technologies, frameworks and libraries are used in this project:
|
||||
|
||||
## Credits
|
||||
The following libraries and frameworks are used in this project:
|
||||
|
||||
- [C#](https://docs.microsoft.com/en-us/dotnet/csharp/) - A simple, modern, object-oriented, and type-safe programming language.
|
||||
- [ASP.NET Core](https://dotnet.microsoft.com/apps/aspnet) - An open-source framework for building modern, cloud-based, internet-connected applications.
|
||||
- [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) - A framework for building interactive web UIs using C# instead of JavaScript.
|
||||
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) - A lightweight, extensible, open-source and cross-platform version of the popular Entity Framework data access technology.
|
||||
- [Blazor WASM](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) - A framework for building interactive web UIs using C# instead of JavaScript. It's a single-page app framework that runs in the browser via WebAssembly.
|
||||
- [Playwright](https://playwright.dev/) - A Node.js library to automate Chromium, Firefox and WebKit with a single API. Used for end-to-end testing.
|
||||
- [Docker](https://www.docker.com/) - A platform for building, sharing, and running containerized applications.
|
||||
- [SQLite](https://www.sqlite.org/index.html) - A C-language library that implements a small, fast, self-contained, high-reliability, full-featured, SQL database engine.
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - A utility-first CSS framework for rapidly building custom designs.
|
||||
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library.
|
||||
- [Flowbite](https://flowbite.com/) - A free and open-source UI component library based on Tailwind CSS.
|
||||
- [Konscious.Security.Cryptography](https://github.com/kmaragon/Konscious.Security.Cryptography) - A .NET library that implements Argon2id, a memory-hard password hashing algorithm.
|
||||
- [SRP.net](https://github.com/secure-remote-password/srp.net) - SRP6a Secure Remote Password protocol for secure password authentication.
|
||||
- [SqliteWasmHelper](https://github.com/JeremyLikness/SqliteWasmHelper) - The AliasVault SQLite WASM implementation is loosely based on this library.
|
||||
|
||||
@@ -3,10 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.10.34928.147
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault", "src\AliasVault\AliasVault.csproj", "{BD2050C0-DC26-4777-9514-546525307370}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasDb", "src\AliasDb\AliasDb.csproj", "{64F47C9A-FE69-4793-B469-28BAADEC6706}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasGenerators", "src\AliasGenerators\AliasGenerators.csproj", "{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}"
|
||||
@@ -27,20 +23,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{29DE523D
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests", "src\Tests\AliasVault.E2ETests\AliasVault.E2ETests.csproj", "{AF013D08-1BF6-4E23-87D2-37F614BE7952}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Database", "Database", "{5F7417F6-4388-49CC-9511-ED63C4A6488A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasServerDb", "src\Databases\AliasServerDb\AliasServerDb.csproj", "{1277105D-50CD-4CE0-9C2C-549F46867E54}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasClientDb", "src\Databases\AliasClientDb\AliasClientDb.csproj", "{FE10F294-817F-477E-A24F-8597A15AF0B5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AliasVault.E2ETests.WebApp.Server", "src\Tests\Server\AliasVault.E2ETests.WebApp.Server\AliasVault.E2ETests.WebApp.Server.csproj", "{DD1F496F-CF10-47D1-A57F-5FA256479332}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Server", "Server", "{607945F3-9896-4544-99EC-F3496CF4D36B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsvImportExport", "src\Utilities\CsvImportExport\CsvImportExport.csproj", "{A9C9A606-C87E-4298-AB32-09B1884D7487}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BD2050C0-DC26-4777-9514-546525307370}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{64F47C9A-FE69-4793-B469-28BAADEC6706}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{78E84B4E-57D1-491A-8F4E-9879AE49DE0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -73,6 +73,22 @@ Global
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -82,5 +98,13 @@ Global
|
||||
{8E6A418A-B305-465D-857D-49953605C18E} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{427EA8E2-EA76-467E-A6BC-201EFE40C0D0} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
{AF013D08-1BF6-4E23-87D2-37F614BE7952} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{1277105D-50CD-4CE0-9C2C-549F46867E54} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
{FE10F294-817F-477E-A24F-8597A15AF0B5} = {5F7417F6-4388-49CC-9511-ED63C4A6488A}
|
||||
{DD1F496F-CF10-47D1-A57F-5FA256479332} = {607945F3-9896-4544-99EC-F3496CF4D36B}
|
||||
{607945F3-9896-4544-99EC-F3496CF4D36B} = {29DE523D-EEF2-41E9-AC12-F20D8D02BEBB}
|
||||
{A9C9A606-C87E-4298-AB32-09B1884D7487} = {01AB9389-2F89-4F8E-A688-BF4BF1FC42C8}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FEE82475-C009-4762-8113-A6563D9DC49E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -10,17 +10,6 @@ services:
|
||||
environment:
|
||||
- API_URL=http://localhost:81
|
||||
|
||||
server:
|
||||
image: aliasvault-server
|
||||
build:
|
||||
context: .
|
||||
dockerfile: src/AliasVault/Dockerfile
|
||||
ports:
|
||||
- "82:8082"
|
||||
volumes:
|
||||
- ./database:/database
|
||||
restart: always
|
||||
|
||||
api:
|
||||
image: aliasvault-api
|
||||
build:
|
||||
@@ -30,4 +19,6 @@ services:
|
||||
- "81:8081"
|
||||
volumes:
|
||||
- ./database:/database
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
5
docs/README.md
Normal file
5
docs/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Documentation
|
||||
This is the documentation for the AliasVault project.
|
||||
|
||||
## Description
|
||||
TODO: Work in progress.
|
||||
18
docs/misc/configure-sqlite-wasm.md
Normal file
18
docs/misc/configure-sqlite-wasm.md
Normal file
@@ -0,0 +1,18 @@
|
||||
To configure SQLite for use with WebAssembly follow these steps:
|
||||
|
||||
1. Add NuGet package
|
||||
```
|
||||
dotnet add package SQLitePCLRaw.bundle_e_sqlite3
|
||||
```
|
||||
|
||||
2. Modify .csproj and add the following:
|
||||
```xml
|
||||
<PropertyGroup>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
</PropertyGroup>
|
||||
```
|
||||
|
||||
3. Make sure the "wasm-tools" workload is installed on the local machine in order to build the project:
|
||||
```
|
||||
dotnet workload install wasm-tools
|
||||
```
|
||||
12
docs/misc/upgrade-ef-client-model.md
Normal file
12
docs/misc/upgrade-ef-client-model.md
Normal file
@@ -0,0 +1,12 @@
|
||||
To upgrade the AliasClientDb EF model, follow these steps:
|
||||
|
||||
1. Make changes to the AliasClientDb EF model in the `AliasClientDb` project.
|
||||
2. Create a new migration by running the following command in the `AliasClientDb` project:
|
||||
|
||||
```bash
|
||||
# Important: make sure the migration name is prefixed by the Semver version number of the release.
|
||||
# For example, if the release version is 1.0.0, the migration name should be `1.0.0-<migration-name>`.
|
||||
dotnet ef migrations add "1.0.0-<migration-name>"
|
||||
```
|
||||
4. On the next login of a user, they will be prompted (required) to upgrade their database schema to the latest version.
|
||||
Make sure to manually test this.
|
||||
78
init.sh
Executable file
78
init.sh
Executable file
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Define colors for CLI output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BLUE='\033[0;34m'
|
||||
MAGENTA='\033[0;35m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Define the path to the .env and .env.example files
|
||||
ENV_FILE=".env"
|
||||
ENV_EXAMPLE_FILE=".env.example"
|
||||
|
||||
# Function to generate a new 32-character JWT key
|
||||
generate_jwt_key() {
|
||||
dd if=/dev/urandom bs=1 count=32 2>/dev/null | base64 | head -c 32
|
||||
}
|
||||
|
||||
# Function to create .env file from .env.example if it doesn't exist
|
||||
create_env_file() {
|
||||
if [ ! -f "$ENV_FILE" ]; then
|
||||
if [ -f "$ENV_EXAMPLE_FILE" ]; then
|
||||
cp "$ENV_EXAMPLE_FILE" "$ENV_FILE"
|
||||
printf "${GREEN}> .env file created from .env.example.${NC}\n"
|
||||
else
|
||||
touch "$ENV_FILE"
|
||||
printf "${YELLOW}> .env file created as empty because .env.example was not found.${NC}\n"
|
||||
fi
|
||||
else
|
||||
printf "${CYAN}> .env file already exists.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check and populate the .env file with JWT_KEY
|
||||
populate_jwt_key() {
|
||||
if ! grep -q "^JWT_KEY=" "$ENV_FILE" || [ -z "$(grep "^JWT_KEY=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
|
||||
printf "${YELLOW}JWT_KEY not found or empty in $ENV_FILE. Generating a new JWT key...${NC}\n"
|
||||
JWT_KEY=$(generate_jwt_key)
|
||||
if grep -q "^JWT_KEY=" "$ENV_FILE"; then
|
||||
awk -v key="$JWT_KEY" '/^JWT_KEY=/ {$0="JWT_KEY="key} 1' "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
|
||||
else
|
||||
printf "JWT_KEY=${JWT_KEY}" >> "$ENV_FILE\n"
|
||||
fi
|
||||
printf "${GREEN}> JWT_KEY has been added to $ENV_FILE.${NC}\n"
|
||||
else
|
||||
printf "${CYAN}> JWT_KEY already exists and has a value in $ENV_FILE.${NC}\n"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to print the CLI logo
|
||||
print_logo() {
|
||||
printf "${MAGENTA}\n"
|
||||
printf "=========================================================\n"
|
||||
printf " _ _ __ __ _ _ \n"
|
||||
printf " /\ | (_) \ \ / / | | | \n"
|
||||
printf " / \ | |_ __ _ __\ \ / /_ _ _ _| | |_\n"
|
||||
printf " / /\ \ | | |/ _ / __\ \/ / _ | | | | | __|\n"
|
||||
printf " / ____ \| | | (_| \__ \\ / (_| | |_| | | |_ \n"
|
||||
printf " /_/ \_\_|_|\__,_|___/ \/ \__,_|\__,_|_|\__|\n"
|
||||
printf "\n"
|
||||
printf "=========================================================\n"
|
||||
printf "${NC}\n"
|
||||
}
|
||||
|
||||
# Run the functions and print status
|
||||
print_logo
|
||||
printf "${BLUE}Initializing AliasVault...${NC}\n"
|
||||
create_env_file
|
||||
populate_jwt_key
|
||||
printf "${BLUE}Initialization complete.${NC}\n"
|
||||
printf "\n"
|
||||
printf "To build the images and start the containers, run the following command:\n"
|
||||
printf "\n"
|
||||
printf "${CYAN}$ docker compose up -d --build --force-recreate${NC}\n"
|
||||
printf "\n"
|
||||
printf "\n"
|
||||
@@ -12,7 +12,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -15,6 +15,10 @@ 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()
|
||||
@@ -23,10 +27,7 @@ public class FigIdentityGenerator : IIdentityGenerator
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var json = await response.Content.ReadAsStringAsync();
|
||||
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
});
|
||||
var identity = JsonSerializer.Deserialize<Identity.Models.Identity>(json, JsonSerializerOptions);
|
||||
|
||||
if (identity is null)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -18,6 +18,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
|
||||
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -33,9 +35,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AliasDb\AliasDb.csproj" />
|
||||
<ProjectReference Include="..\AliasGenerators\AliasGenerators.csproj" />
|
||||
<ProjectReference Include="..\AliasVault.Shared\AliasVault.Shared.csproj" />
|
||||
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
|
||||
<ProjectReference Include="..\Utilities\Cryptography\Cryptography.csproj" />
|
||||
<ProjectReference Include="..\Utilities\FaviconExtractor\FaviconExtractor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AliasController.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.Api.Controllers;
|
||||
|
||||
using System.Globalization;
|
||||
using AliasDb;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Identity = AliasVault.Shared.Models.WebApi.Identity;
|
||||
using Service = AliasVault.Shared.Models.WebApi.Service;
|
||||
|
||||
/// <summary>
|
||||
/// Alias controller for handling CRUD operations on the database for alias entities.
|
||||
/// </summary>
|
||||
/// <param name="context">DbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
public class AliasController(AliasDbContext context, UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Get all alias items for the current user.
|
||||
/// </summary>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
[HttpGet("items")]
|
||||
public async Task<IActionResult> GetItems()
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// Logic to retrieve items for the user.
|
||||
var aliases = await context.Logins
|
||||
.Include(x => x.Identity)
|
||||
.Include(x => x.Service)
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.Select(x => new AliasListEntry
|
||||
{
|
||||
Id = x.Id,
|
||||
Logo = x.Service.Logo,
|
||||
Service = x.Service.Name ?? "n/a",
|
||||
CreateDate = x.CreatedAt,
|
||||
})
|
||||
|
||||
.ToListAsync();
|
||||
|
||||
return Ok(aliases);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a single alias item by its ID.
|
||||
/// </summary>
|
||||
/// <param name="aliasId">ID of the alias.</param>
|
||||
/// <returns>Alias object as JSON.</returns>
|
||||
[HttpGet("{aliasId}")]
|
||||
public async Task<IActionResult> GetAlias(Guid aliasId)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var aliasObject = await context.Logins
|
||||
.Include(x => x.Passwords)
|
||||
.Include(x => x.Identity)
|
||||
.Include(x => x.Service)
|
||||
.Where(x => x.Id == aliasId)
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.Select(x => new Alias()
|
||||
{
|
||||
Service = new Service()
|
||||
{
|
||||
Name = x.Service.Name ?? "n/a",
|
||||
Url = x.Service.Url,
|
||||
LogoUrl = string.Empty,
|
||||
CreatedAt = x.Service.CreatedAt,
|
||||
UpdatedAt = x.Service.UpdatedAt,
|
||||
},
|
||||
Identity = new Identity()
|
||||
{
|
||||
NickName = x.Identity.NickName,
|
||||
FirstName = x.Identity.FirstName,
|
||||
LastName = x.Identity.LastName,
|
||||
BirthDate = x.Identity.BirthDate.ToString("yyyy-MM-dd"),
|
||||
Gender = x.Identity.Gender,
|
||||
AddressStreet = x.Identity.AddressStreet,
|
||||
AddressCity = x.Identity.AddressCity,
|
||||
AddressState = x.Identity.AddressState,
|
||||
AddressZipCode = x.Identity.AddressZipCode,
|
||||
AddressCountry = x.Identity.AddressCountry,
|
||||
Hobbies = x.Identity.Hobbies,
|
||||
EmailPrefix = x.Identity.EmailPrefix,
|
||||
PhoneMobile = x.Identity.PhoneMobile,
|
||||
BankAccountIBAN = x.Identity.BankAccountIBAN,
|
||||
CreatedAt = x.Identity.CreatedAt,
|
||||
UpdatedAt = x.Identity.UpdatedAt,
|
||||
},
|
||||
Password = new AliasVault.Shared.Models.WebApi.Password()
|
||||
{
|
||||
Value = x.Passwords.First().Value ?? string.Empty,
|
||||
Description = string.Empty,
|
||||
CreatedAt = x.Passwords.First().CreatedAt,
|
||||
UpdatedAt = x.Passwords.First().UpdatedAt,
|
||||
},
|
||||
CreateDate = x.CreatedAt,
|
||||
LastUpdate = x.UpdatedAt,
|
||||
})
|
||||
.FirstAsync();
|
||||
|
||||
return Ok(aliasObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Insert a new alias to the database.
|
||||
/// </summary>
|
||||
/// <param name="model">Alias model.</param>
|
||||
/// <returns>ID of newly inserted alias.</returns>
|
||||
[HttpPut("")]
|
||||
public async Task<IActionResult> Insert([FromBody] Alias model)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var login = new Login
|
||||
{
|
||||
UserId = user.Id,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
Identity = new AliasDb.Identity()
|
||||
{
|
||||
NickName = model.Identity.NickName,
|
||||
FirstName = model.Identity.FirstName,
|
||||
LastName = model.Identity.LastName,
|
||||
BirthDate = DateTime.Parse(model.Identity.BirthDate ?? "1900-01-01", new CultureInfo("en-US")),
|
||||
Gender = model.Identity.Gender,
|
||||
AddressStreet = model.Identity.AddressStreet,
|
||||
AddressCity = model.Identity.AddressCity,
|
||||
AddressState = model.Identity.AddressState,
|
||||
AddressZipCode = model.Identity.AddressZipCode,
|
||||
AddressCountry = model.Identity.AddressCountry,
|
||||
Hobbies = model.Identity.Hobbies,
|
||||
EmailPrefix = model.Identity.EmailPrefix,
|
||||
PhoneMobile = model.Identity.PhoneMobile,
|
||||
BankAccountIBAN = model.Identity.BankAccountIBAN,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
},
|
||||
};
|
||||
|
||||
login.Passwords.Add(new AliasDb.Password()
|
||||
{
|
||||
Value = model.Password.Value,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
});
|
||||
|
||||
login.Service = new AliasDb.Service()
|
||||
{
|
||||
Name = model.Service.Name,
|
||||
Url = model.Service.Url,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await context.Logins.AddAsync(login);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return Ok(login.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing alias entry in the database.
|
||||
/// </summary>
|
||||
/// <param name="aliasId">The alias ID to update.</param>
|
||||
/// <param name="model">Alias model.</param>
|
||||
/// <returns>ID of updated alias entry.</returns>
|
||||
[HttpPost("{aliasId}")]
|
||||
public async Task<IActionResult> Update(Guid aliasId, [FromBody] Alias model)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// Get the existing entry.
|
||||
var login = await context.Logins
|
||||
.Include(x => x.Identity)
|
||||
.Include(x => x.Service)
|
||||
.Include(x => x.Passwords)
|
||||
.Where(x => x.Id == aliasId)
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.FirstAsync();
|
||||
|
||||
login.UpdatedAt = DateTime.UtcNow;
|
||||
login.Identity.NickName = model.Identity.NickName;
|
||||
login.Identity.FirstName = model.Identity.FirstName;
|
||||
login.Identity.LastName = model.Identity.LastName;
|
||||
login.Identity.BirthDate = DateTime.Parse(model.Identity.BirthDate ?? "1900-01-01", new CultureInfo("en-US"));
|
||||
login.Identity.Gender = model.Identity.Gender;
|
||||
login.Identity.AddressStreet = model.Identity.AddressStreet;
|
||||
login.Identity.AddressCity = model.Identity.AddressCity;
|
||||
login.Identity.AddressState = model.Identity.AddressState;
|
||||
login.Identity.AddressZipCode = model.Identity.AddressZipCode;
|
||||
login.Identity.AddressCountry = model.Identity.AddressCountry;
|
||||
login.Identity.Hobbies = model.Identity.Hobbies;
|
||||
login.Identity.EmailPrefix = model.Identity.EmailPrefix;
|
||||
login.Identity.PhoneMobile = model.Identity.PhoneMobile;
|
||||
login.Identity.BankAccountIBAN = model.Identity.BankAccountIBAN;
|
||||
|
||||
login.Passwords.First().Value = model.Password.Value;
|
||||
login.Passwords.First().UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
login.Service.Name = model.Service.Name;
|
||||
login.Service.Url = model.Service.Url;
|
||||
login.Service.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return Ok(login.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete an existing alias entry from the database.
|
||||
/// </summary>
|
||||
/// <param name="aliasId">ID of the alias to delete.</param>
|
||||
/// <returns>HTTP status code.</returns>
|
||||
[HttpDelete("{aliasId}")]
|
||||
public async Task<IActionResult> Delete(Guid aliasId)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var login = await context.Logins
|
||||
.Where(x => x.Id == aliasId)
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.FirstAsync();
|
||||
|
||||
context.Logins.Remove(login);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -11,39 +11,99 @@ using System.IdentityModel.Tokens.Jwt;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using AliasDb;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Models;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using Asp.Versioning;
|
||||
using Cryptography.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
|
||||
/// <summary>
|
||||
/// Auth controller for handling authentication.
|
||||
/// </summary>
|
||||
/// <param name="context">AliasDbContext instance.</param>
|
||||
/// <param name="context">AliasServerDbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
/// <param name="signInManager">SignInManager instance.</param>
|
||||
/// <param name="configuration">IConfiguration instance.</param>
|
||||
[Route("api/[controller]")]
|
||||
/// <param name="cache">IMemoryCache instance for persisting SRP values during multi-step login process.</param>
|
||||
/// <param name="timeProvider">ITimeProvider instance. This returns the time which can be mutated for testing..</param>
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
public class AuthController(AliasDbContext context, UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager, IConfiguration configuration) : ControllerBase
|
||||
[ApiVersion("1")]
|
||||
public class AuthController(AliasServerDbContext context, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Error message for invalid email or password.
|
||||
/// </summary>
|
||||
public static readonly string[] InvalidEmailOrPasswordError = { "Invalid email or password. Please try again." };
|
||||
|
||||
/// <summary>
|
||||
/// Login endpoint used to process login attempt using credentials.
|
||||
/// </summary>
|
||||
/// <param name="model">Login model.</param>
|
||||
/// <returns>IActionResult.</returns>
|
||||
[HttpPost("login")]
|
||||
public async Task<IActionResult> Login([FromBody] LoginModel model)
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest model)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(model.Email);
|
||||
if (user != null && await userManager.CheckPasswordAsync(user, model.Password))
|
||||
if (user == null)
|
||||
{
|
||||
var tokenModel = await GenerateNewTokenForUser(user);
|
||||
return Ok(tokenModel);
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400));
|
||||
}
|
||||
|
||||
return Unauthorized();
|
||||
// Server creates ephemeral and sends to client
|
||||
var ephemeral = Cryptography.Srp.GenerateEphemeralServer(user.Verifier);
|
||||
|
||||
// Store the server ephemeral in memory cache for Validate() endpoint to use.
|
||||
cache.Set(model.Email, ephemeral.Secret, TimeSpan.FromMinutes(5));
|
||||
|
||||
return Ok(new LoginResponse(user.Salt, ephemeral.Public));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate endpoint used to validate the client's proof and generate the server's proof.
|
||||
/// </summary>
|
||||
/// <param name="model">ValidateLoginRequest model.</param>
|
||||
/// <returns>IActionResult.</returns>
|
||||
[HttpPost("validate")]
|
||||
public async Task<IActionResult> Validate([FromBody] ValidateLoginRequest model)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400));
|
||||
}
|
||||
|
||||
if (!cache.TryGetValue(model.Email, out var serverSecretEphemeral) || !(serverSecretEphemeral is string))
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var serverSession = Cryptography.Srp.DeriveSessionServer(
|
||||
serverSecretEphemeral.ToString() ?? string.Empty,
|
||||
model.ClientPublicEphemeral,
|
||||
user.Salt,
|
||||
model.Email,
|
||||
user.Verifier,
|
||||
model.ClientSessionProof);
|
||||
|
||||
// If above does not throw an exception., then the client's proof is valid, and we can issue the JWT token.
|
||||
var tokenModel = await GenerateNewTokensForUser(user);
|
||||
|
||||
// Return server proof for optional client check and token.
|
||||
return Ok(new ValidateLoginResponse(serverSession.Proof, tokenModel));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return BadRequest(ServerValidationErrorResponse.Create(InvalidEmailOrPasswordError, 400));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,7 +130,7 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
// Remove any existing refresh tokens for this user and device.
|
||||
var deviceIdentifier = GenerateDeviceIdentifier(Request);
|
||||
var existingToken = context.AspNetUserRefreshTokens.Where(t => t.UserId == user.Id && t.DeviceIdentifier == deviceIdentifier).FirstOrDefault();
|
||||
if (existingToken == null || existingToken.Value != tokenModel.RefreshToken || existingToken.ExpireDate < DateTime.Now)
|
||||
if (existingToken == null || existingToken.Value != tokenModel.RefreshToken || existingToken.ExpireDate < timeProvider.UtcNow)
|
||||
{
|
||||
return Unauthorized("Refresh token expired");
|
||||
}
|
||||
@@ -87,8 +147,8 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
UserId = user.Id,
|
||||
DeviceIdentifier = deviceIdentifier,
|
||||
Value = newRefreshToken,
|
||||
ExpireDate = DateTime.Now.AddDays(30),
|
||||
CreatedAt = DateTime.Now,
|
||||
ExpireDate = timeProvider.UtcNow.AddDays(30),
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
@@ -137,10 +197,10 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
/// <param name="model">Register model.</param>
|
||||
/// <returns>IActionResult.</returns>
|
||||
[HttpPost("register")]
|
||||
public async Task<IActionResult> Register([FromBody] RegisterModel model)
|
||||
public async Task<IActionResult> Register([FromBody] SrpSignup model)
|
||||
{
|
||||
var user = new IdentityUser { UserName = model.Email, Email = model.Email };
|
||||
var result = await userManager.CreateAsync(user, model.Password);
|
||||
var user = new AliasVaultUser { UserName = model.Email, Email = model.Email, Salt = model.Salt, Verifier = model.Verifier };
|
||||
var result = await userManager.CreateAsync(user);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
@@ -148,55 +208,63 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
await signInManager.SignInAsync(user, isPersistent: false);
|
||||
|
||||
// Return the token.
|
||||
var tokenModel = await GenerateNewTokenForUser(user);
|
||||
var tokenModel = await GenerateNewTokensForUser(user);
|
||||
return Ok(tokenModel);
|
||||
}
|
||||
else
|
||||
|
||||
var errors = result.Errors.Select(e => e.Description).ToArray();
|
||||
return BadRequest(ServerValidationErrorResponse.Create(errors, 400));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a device identifier based on request headers. This is used to associate refresh tokens
|
||||
/// with a specific device for a specific user.
|
||||
///
|
||||
/// NOTE: current implementation means that only one refresh token can be valid for a
|
||||
/// specific user/device combo at a time. The identifier generation could be made more unique in the future
|
||||
/// to prevent any unwanted conflicts.
|
||||
/// </summary>
|
||||
/// <param name="request">The HttpRequest instance for the request that the client used.</param>
|
||||
/// <returns>Unique device identifier as string.</returns>
|
||||
private static string GenerateDeviceIdentifier(HttpRequest request)
|
||||
{
|
||||
var userAgent = request.Headers.UserAgent.ToString();
|
||||
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
|
||||
|
||||
var rawIdentifier = $"{userAgent}|{acceptLanguage}";
|
||||
return rawIdentifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the JWT key from the environment variables.
|
||||
/// </summary>
|
||||
/// <returns>JWT key as string.</returns>
|
||||
/// <exception cref="KeyNotFoundException">Thrown if environment variable does not exist.</exception>
|
||||
private static string GetJwtKey()
|
||||
{
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (jwtKey is null)
|
||||
{
|
||||
return BadRequest(result.Errors);
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
|
||||
return jwtKey;
|
||||
}
|
||||
|
||||
private string GenerateJwtToken(IdentityUser user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, user.Id ?? string.Empty),
|
||||
new(ClaimTypes.Name, user.UserName ?? string.Empty),
|
||||
new(ClaimTypes.Email, user.Email ?? string.Empty),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: configuration["Jwt:Issuer"] ?? string.Empty,
|
||||
audience: configuration["Jwt:Issuer"] ?? string.Empty,
|
||||
claims: claims,
|
||||
expires: DateTime.Now.AddMinutes(30),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
private string GenerateRefreshToken()
|
||||
{
|
||||
var randomNumber = new byte[32];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
|
||||
rng.GetBytes(randomNumber);
|
||||
return Convert.ToBase64String(randomNumber);
|
||||
}
|
||||
|
||||
private ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
|
||||
/// <summary>
|
||||
/// Get the principal from an expired token. This is used to validate the token and extract the user.
|
||||
/// </summary>
|
||||
/// <param name="token">The expired token as string.</param>
|
||||
/// <returns>Claims principal.</returns>
|
||||
/// <exception cref="SecurityTokenException">Thrown if provided token is invalid.</exception>
|
||||
private static ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
|
||||
{
|
||||
var tokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
ValidateAudience = false,
|
||||
ValidateIssuer = false,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty)),
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GetJwtKey())),
|
||||
ValidateLifetime = false,
|
||||
};
|
||||
|
||||
@@ -210,17 +278,57 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
return principal;
|
||||
}
|
||||
|
||||
private string GenerateDeviceIdentifier(HttpRequest request)
|
||||
/// <summary>
|
||||
/// Generate a refresh token for a user. This token is used to request a new access token when the current
|
||||
/// access token expires. The refresh token is long-lived by design.
|
||||
/// </summary>
|
||||
/// <returns>Random string to be used as refresh token.</returns>
|
||||
private static string GenerateRefreshToken()
|
||||
{
|
||||
// TODO: Add more headers to the device identifier or let client send a unique identifier instead.
|
||||
var userAgent = request.Headers.UserAgent.ToString();
|
||||
var acceptLanguage = request.Headers.AcceptLanguage.ToString();
|
||||
var randomNumber = new byte[32];
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
|
||||
var rawIdentifier = $"{userAgent}|{acceptLanguage}";
|
||||
return rawIdentifier;
|
||||
rng.GetBytes(randomNumber);
|
||||
return Convert.ToBase64String(randomNumber);
|
||||
}
|
||||
|
||||
private async Task<TokenModel> GenerateNewTokenForUser(IdentityUser user)
|
||||
/// <summary>
|
||||
/// Generate a Jwt access token for a user. This token is used to authenticate the user for a limited time
|
||||
/// and is short-lived by design. With the separate refresh token, the user can request a new access token
|
||||
/// when this access token expires.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to generate the Jwt access token for.</param>
|
||||
/// <returns>Access token as string.</returns>
|
||||
private string GenerateJwtToken(AliasVaultUser user)
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(ClaimTypes.NameIdentifier, user.Id),
|
||||
new(ClaimTypes.Name, user.UserName ?? string.Empty),
|
||||
new(ClaimTypes.Email, user.Email ?? string.Empty),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||||
};
|
||||
|
||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(GetJwtKey()));
|
||||
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||||
|
||||
var token = new JwtSecurityToken(
|
||||
issuer: configuration["Jwt:Issuer"] ?? string.Empty,
|
||||
audience: configuration["Jwt:Issuer"] ?? string.Empty,
|
||||
claims: claims,
|
||||
expires: timeProvider.UtcNow.AddMinutes(10),
|
||||
signingCredentials: creds);
|
||||
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new access and refresh token for a user and persists the refresh token
|
||||
/// to the database.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to generate the tokens for.</param>
|
||||
/// <returns>TokenModel which includes new access and refresh token.</returns>
|
||||
private async Task<TokenModel> GenerateNewTokensForUser(AliasVaultUser user)
|
||||
{
|
||||
var token = GenerateJwtToken(user);
|
||||
var refreshToken = GenerateRefreshToken();
|
||||
@@ -239,8 +347,8 @@ public class AuthController(AliasDbContext context, UserManager<IdentityUser> us
|
||||
UserId = user.Id,
|
||||
DeviceIdentifier = deviceIdentifier,
|
||||
Value = refreshToken,
|
||||
ExpireDate = DateTime.Now.AddDays(30),
|
||||
CreatedAt = DateTime.Now,
|
||||
ExpireDate = timeProvider.UtcNow.AddDays(30),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
using System.Security.Claims;
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
@@ -15,16 +17,16 @@ using Microsoft.AspNetCore.Mvc;
|
||||
/// Base controller for requests that require authentication.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/v{version:apiVersion}/[controller]")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class AuthenticatedRequestController(UserManager<IdentityUser> userManager) : ControllerBase
|
||||
public class AuthenticatedRequestController(UserManager<AliasVaultUser> userManager) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the current authenticated user.
|
||||
/// </summary>
|
||||
/// <returns>IdentityUser object for current user.</returns>
|
||||
protected async Task<IdentityUser?> GetCurrentUserAsync()
|
||||
/// <returns>AliasVaultUser object for current user.</returns>
|
||||
protected async Task<AliasVaultUser?> GetCurrentUserAsync()
|
||||
{
|
||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Unable to find user ID.");
|
||||
return await userManager.FindByIdAsync(userId);
|
||||
|
||||
43
src/AliasVault.Api/Controllers/FaviconController.cs
Normal file
43
src/AliasVault.Api/Controllers/FaviconController.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="FaviconController.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.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Shared.Models;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for retrieving favicons from external websites.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class FaviconController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxies the request to the identity generator to generate a random identity.
|
||||
/// </summary>
|
||||
/// <param name="url">URL to extract the favicon from.</param>
|
||||
/// <returns>Identity model.</returns>
|
||||
[HttpGet("Extract")]
|
||||
public async Task<IActionResult> Extract(string url)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// Get the favicon from the URL.
|
||||
var image = await FaviconExtractor.FaviconExtractor.GetFaviconAsync(url);
|
||||
|
||||
// Return the favicon as base64 string of image representation.
|
||||
return Ok(new FaviconExtractModel { Image = image });
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
namespace AliasVault.Api.Controllers;
|
||||
|
||||
using AliasGenerators.Identity;
|
||||
using AliasGenerators.Identity.Implementations;
|
||||
using AliasServerDb;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -15,13 +17,14 @@ using Microsoft.AspNetCore.Mvc;
|
||||
/// Controller for identity generation.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
public class IdentityController(UserManager<IdentityUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
[ApiVersion("1")]
|
||||
public class IdentityController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Proxies the request to the identity generator to generate a random identity.
|
||||
/// </summary>
|
||||
/// <returns>Identity model.</returns>
|
||||
[HttpGet("generate")]
|
||||
[HttpGet("Generate")]
|
||||
public async Task<IActionResult> Generate()
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
@@ -30,7 +33,7 @@ public class IdentityController(UserManager<IdentityUser> userManager) : Authent
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
IIdentityGenerator identityGenerator = new FigIdentityGenerator();
|
||||
var identityGenerator = new FigIdentityGenerator();
|
||||
return Ok(await identityGenerator.GenerateRandomIdentityAsync());
|
||||
}
|
||||
}
|
||||
|
||||
53
src/AliasVault.Api/Controllers/RootController.cs
Normal file
53
src/AliasVault.Api/Controllers/RootController.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RootController.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.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Root controller that contains health check endpoints.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("/")]
|
||||
public class RootController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Root endpoint that returns a 200 OK if the database connection is successful
|
||||
/// and the DB migrations are up-to-date.
|
||||
/// </summary>
|
||||
/// <returns>Http 200 if database connection is successful.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType<int>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<int>(StatusCodes.Status500InternalServerError)]
|
||||
public IActionResult Get()
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var dbContext = new AliasServerDbContext())
|
||||
{
|
||||
var appliedMigrations = dbContext.Database.GetAppliedMigrations();
|
||||
var allMigrations = dbContext.Database.GetMigrations();
|
||||
|
||||
if (allMigrations.Except(appliedMigrations).Any())
|
||||
{
|
||||
// There are pending migrations
|
||||
return StatusCode(500, "There are pending migrations. Please run 'dotnet ef database update' to apply them.");
|
||||
}
|
||||
|
||||
// Database is up to date
|
||||
return Ok("OK");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return StatusCode(500, "Internal server error");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/AliasVault.Api/Controllers/TestController.cs
Normal file
31
src/AliasVault.Api/Controllers/TestController.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="TestController.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.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
/// <summary>
|
||||
/// Test controller that contains test endpoints called by pages on the client for E2E testing purposes.
|
||||
/// </summary>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class TestController(UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Authenticated test request.
|
||||
/// </summary>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
[HttpGet("")]
|
||||
public IActionResult TestCall()
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
115
src/AliasVault.Api/Controllers/VaultController.cs
Normal file
115
src/AliasVault.Api/Controllers/VaultController.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="VaultController.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.Api.Controllers;
|
||||
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Vault;
|
||||
using AliasVault.Api.Vault.RetentionRules;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Vault controller for handling CRUD operations on the database for encrypted vault entities.
|
||||
/// </summary>
|
||||
/// <param name="context">DbContext instance.</param>
|
||||
/// <param name="userManager">UserManager instance.</param>
|
||||
/// <param name="timeProvider">ITimeProvider instance.</param>
|
||||
[ApiVersion("1")]
|
||||
public class VaultController(AliasServerDbContext context, UserManager<AliasVaultUser> userManager, ITimeProvider timeProvider) : AuthenticatedRequestController(userManager)
|
||||
{
|
||||
/// <summary>
|
||||
/// Default retention policy for vaults.
|
||||
/// </summary>
|
||||
private readonly RetentionPolicy _retentionPolicy = new()
|
||||
{
|
||||
Rules = new List<IRetentionRule>
|
||||
{
|
||||
new DailyRetentionRule { DaysToKeep = 3 },
|
||||
new WeeklyRetentionRule { WeeksToKeep = 1 },
|
||||
new MonthlyRetentionRule { MonthsToKeep = 1 },
|
||||
new VersionRetentionRule { VersionsToKeep = 3 },
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Get the newest version of the vault for the current user.
|
||||
/// </summary>
|
||||
/// <returns>List of aliases in JSON format.</returns>
|
||||
[HttpGet("")]
|
||||
public async Task<IActionResult> GetVault()
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// Logic to retrieve vault for the user.
|
||||
var vault = await context.Vaults
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.OrderByDescending(x => x.UpdatedAt)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
// If no vault is found on server, return an empty object. This means the client will use an empty vault
|
||||
// as starting point.
|
||||
if (vault == null)
|
||||
{
|
||||
return Ok(new Shared.Models.WebApi.Vault(string.Empty, string.Empty, DateTime.MinValue, DateTime.MinValue));
|
||||
}
|
||||
|
||||
return Ok(new Shared.Models.WebApi.Vault(vault.VaultBlob, vault.Version, vault.CreatedAt, vault.UpdatedAt));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save a new vault to the database for the current user.
|
||||
/// </summary>
|
||||
/// <param name="model">Vault model.</param>
|
||||
/// <returns>IActionResult.</returns>
|
||||
[HttpPost("")]
|
||||
public async Task<IActionResult> Update([FromBody] Shared.Models.WebApi.Vault model)
|
||||
{
|
||||
var user = await GetCurrentUserAsync();
|
||||
if (user == null)
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
// Create new vault entry.
|
||||
var newVault = new AliasServerDb.Vault
|
||||
{
|
||||
UserId = user.Id,
|
||||
VaultBlob = model.Blob,
|
||||
Version = model.Version,
|
||||
CreatedAt = timeProvider.UtcNow,
|
||||
UpdatedAt = timeProvider.UtcNow,
|
||||
};
|
||||
|
||||
// Run the vault retention manager to keep the required vaults according
|
||||
// to the applied retention policies and delete the rest.
|
||||
// We only select the Id and UpdatedAt fields to reduce the amount of data transferred from the database.
|
||||
var existingVaults = await context.Vaults
|
||||
.Where(x => x.UserId == user.Id)
|
||||
.OrderByDescending(v => v.UpdatedAt)
|
||||
.Select(x => new AliasServerDb.Vault { Id = x.Id, UpdatedAt = x.UpdatedAt })
|
||||
.ToListAsync();
|
||||
|
||||
var vaultsToDelete = VaultRetentionManager.ApplyRetention(_retentionPolicy, existingVaults, timeProvider.UtcNow, newVault);
|
||||
|
||||
// Delete vaults that are not needed anymore.
|
||||
context.Vaults.RemoveRange(vaultsToDelete);
|
||||
|
||||
// Add the new vault and commit to database.
|
||||
await context.Vaults.AddAsync(newVault);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ WORKDIR /src
|
||||
|
||||
# Copy the project files and restore dependencies
|
||||
COPY ["src/AliasVault.Api/AliasVault.Api.csproj", "src/AliasVault.Api/"]
|
||||
COPY ["src/AliasDb/AliasDb.csproj", "src/AliasDb/"]
|
||||
COPY ["src/Databases/AliasServerDb/AliasServerDb.csproj", "src/Databases/AliasServerDb/"]
|
||||
COPY ["src/AliasVault.Shared/AliasVault.Shared.csproj", "src/AliasVault.Shared/"]
|
||||
COPY ["src/AliasGenerators/AliasGenerators.csproj", "src/AliasGenerators/"]
|
||||
RUN dotnet restore "src/AliasVault.Api/AliasVault.Api.csproj"
|
||||
@@ -19,17 +19,12 @@ COPY . .
|
||||
|
||||
# Build the WebApi project
|
||||
WORKDIR "/src/src/AliasVault.Api"
|
||||
RUN dotnet build "AliasVault.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
RUN dotnet build "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
|
||||
|
||||
# Publish the application to the /app/publish directory in the container
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "AliasVault.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Create the migration bundle
|
||||
# Install the Entity Framework Core CLI tool and run migrations to create the database
|
||||
RUN dotnet tool install --global dotnet-ef --version 8.0.5
|
||||
RUN /root/.dotnet/tools/dotnet-ef migrations bundle -o /app/migrationbundle
|
||||
RUN dotnet publish "AliasVault.Api.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
40
src/AliasVault.Api/Jwt/TimeValidationJwtBearerEvents.cs
Normal file
40
src/AliasVault.Api/Jwt/TimeValidationJwtBearerEvents.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="TimeValidationJwtBearerEvents.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.Api.Jwt;
|
||||
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
/// <summary>
|
||||
/// JwtBearerEvents implementation that validates the token expiration time based on
|
||||
/// the current time provided by an ITimeProvider. This is used to be able to
|
||||
/// test the token expiration logic in unit tests.
|
||||
/// </summary>
|
||||
public class TimeValidationJwtBearerEvents(ITimeProvider timeProvider) : JwtBearerEvents
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the token expiration time based on the current time provided by the ITimeProvider.
|
||||
/// </summary>
|
||||
/// <param name="context">TokenValidatedContext.</param>
|
||||
/// <returns>Async task.</returns>
|
||||
public override Task TokenValidated(TokenValidatedContext context)
|
||||
{
|
||||
var jwtToken = context.SecurityToken as JsonWebToken;
|
||||
if (jwtToken != null)
|
||||
{
|
||||
var now = timeProvider.UtcNow;
|
||||
if (jwtToken.ValidTo < now)
|
||||
{
|
||||
context.Fail("Token has expired.");
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
using System.Data.Common;
|
||||
using System.Text;
|
||||
using AliasDb;
|
||||
using AliasServerDb;
|
||||
using AliasVault.Api.Jwt;
|
||||
using AliasVault.Shared.Providers.Time;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Data.Sqlite;
|
||||
@@ -18,6 +21,9 @@ using Microsoft.OpenApi.Models;
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
var configuration = builder.Configuration;
|
||||
|
||||
builder.Services.AddSingleton<ITimeProvider, SystemTimeProvider>();
|
||||
builder.Services.AddScoped<TimeValidationJwtBearerEvents>();
|
||||
|
||||
builder.Services.AddLogging(logging =>
|
||||
{
|
||||
logging.AddConsole();
|
||||
@@ -26,7 +32,6 @@ builder.Services.AddLogging(logging =>
|
||||
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
|
||||
});
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddSingleton<DbConnection>(container =>
|
||||
{
|
||||
var configFile = new ConfigurationBuilder()
|
||||
@@ -34,13 +39,13 @@ builder.Services.AddSingleton<DbConnection>(container =>
|
||||
.AddJsonFile("appsettings.json")
|
||||
.Build();
|
||||
|
||||
var connection = new SqliteConnection(configFile.GetConnectionString("AliasDbContext"));
|
||||
var connection = new SqliteConnection(configFile.GetConnectionString("AliasServerDbContext"));
|
||||
connection.Open();
|
||||
|
||||
return connection;
|
||||
});
|
||||
|
||||
builder.Services.AddDbContext<AliasDbContext>((container, options) =>
|
||||
builder.Services.AddDbContext<AliasServerDbContext>((container, options) =>
|
||||
{
|
||||
var connection = container.GetRequiredService<DbConnection>();
|
||||
options.UseSqlite(connection).UseLazyLoadingProxies();
|
||||
@@ -52,7 +57,7 @@ builder.Services.Configure<DataProtectionTokenProviderOptions>(options =>
|
||||
options.TokenLifespan = TimeSpan.FromDays(30);
|
||||
options.Name = "AliasVault";
|
||||
});
|
||||
builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
||||
builder.Services.AddIdentity<AliasVaultUser, IdentityRole>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
@@ -61,11 +66,11 @@ builder.Services.AddIdentity<IdentityUser, IdentityRole>(options =>
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequiredUniqueChars = 0;
|
||||
options.SignIn.RequireConfirmedAccount = false;
|
||||
options.Tokens.ProviderMap.Add("AliasVault", new TokenProviderDescriptor(typeof(DataProtectorTokenProvider<IdentityUser>)));
|
||||
options.Tokens.ProviderMap.Add("AliasVault", new TokenProviderDescriptor(typeof(DataProtectorTokenProvider<AliasVaultUser>)));
|
||||
})
|
||||
.AddEntityFrameworkStores<AliasDbContext>()
|
||||
.AddEntityFrameworkStores<AliasServerDbContext>()
|
||||
.AddDefaultTokenProviders()
|
||||
.AddTokenProvider<DataProtectorTokenProvider<IdentityUser>>("AliasVault");
|
||||
.AddTokenProvider<DataProtectorTokenProvider<AliasVaultUser>>("AliasVault");
|
||||
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
@@ -73,6 +78,12 @@ builder.Services.AddAuthentication(options =>
|
||||
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
|
||||
}).AddJwtBearer(options =>
|
||||
{
|
||||
var jwtKey = Environment.GetEnvironmentVariable("JWT_KEY");
|
||||
if (jwtKey is null)
|
||||
{
|
||||
throw new KeyNotFoundException("JWT_KEY environment variable is not set.");
|
||||
}
|
||||
|
||||
options.IncludeErrorDetails = true;
|
||||
options.TokenValidationParameters = new TokenValidationParameters
|
||||
{
|
||||
@@ -83,9 +94,13 @@ builder.Services.AddAuthentication(options =>
|
||||
ValidateIssuerSigningKey = true,
|
||||
ValidIssuer = configuration["Jwt:Issuer"],
|
||||
ValidAudience = configuration["Jwt:Issuer"],
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"] ?? string.Empty)),
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
|
||||
ClockSkew = TimeSpan.Zero,
|
||||
};
|
||||
|
||||
// Add custom event handler for validating token expiration time in order
|
||||
// to be able to mutate current time for testing the token expiration logic in unit tests.
|
||||
options.EventsType = typeof(TimeValidationJwtBearerEvents);
|
||||
});
|
||||
|
||||
// Configure CORS
|
||||
@@ -99,7 +114,21 @@ builder.Services.AddCors(options =>
|
||||
});
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ReportApiVersions = true;
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "AliasVault API", Version = "v1" });
|
||||
@@ -150,9 +179,9 @@ app.MapControllers();
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var container = scope.ServiceProvider;
|
||||
var db = container.GetRequiredService<AliasDbContext>();
|
||||
var db = container.GetRequiredService<AliasServerDbContext>();
|
||||
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
await db.Database.MigrateAsync();
|
||||
}
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:39952",
|
||||
"sslPort": 44368
|
||||
}
|
||||
},
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://localhost:5092",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"JWT_KEY": "12345678901234567890123456789012"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5092"
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "https://localhost:7223;http://localhost:5092",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
"ASPNETCORE_ENVIRONMENT": "Development",
|
||||
"JWT_KEY": "12345678901234567890123456789012"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "https://localhost:7223;http://localhost:5092"
|
||||
},
|
||||
"IIS Express": {
|
||||
"commandName": "IISExpress",
|
||||
@@ -37,5 +30,14 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:39952",
|
||||
"sslPort": 44368
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
21
src/AliasVault.Api/Vault/RetentionPolicy.cs
Normal file
21
src/AliasVault.Api/Vault/RetentionPolicy.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RetentionPolicy.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.Api.Vault;
|
||||
|
||||
using AliasVault.Api.Vault.RetentionRules;
|
||||
|
||||
/// <summary>
|
||||
/// The retention policy that contains one or more retention rules.
|
||||
/// </summary>
|
||||
public class RetentionPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the rules that this policy consists of.
|
||||
/// </summary>
|
||||
public List<IRetentionRule> Rules { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="DailyRetentionRule.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.Api.Vault.RetentionRules;
|
||||
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Daily retention rule that keeps the latest vault for each day.
|
||||
/// </summary>
|
||||
public class DailyRetentionRule : IRetentionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets amount of days to keep vault.
|
||||
/// </summary>
|
||||
public int DaysToKeep { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IRetentionRule.ApplyRule"/>
|
||||
public IEnumerable<Vault> ApplyRule(List<Vault> vaults, DateTime now)
|
||||
{
|
||||
// For the specified amount of days, take last vault per day.
|
||||
return vaults
|
||||
.GroupBy(x => x.UpdatedAt.Date)
|
||||
.Select(g => g.OrderByDescending(x => x.UpdatedAt).First())
|
||||
.OrderByDescending(x => x.UpdatedAt)
|
||||
.Take(DaysToKeep);
|
||||
}
|
||||
}
|
||||
24
src/AliasVault.Api/Vault/RetentionRules/IRetentionRule.cs
Normal file
24
src/AliasVault.Api/Vault/RetentionRules/IRetentionRule.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="IRetentionRule.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.Api.Vault.RetentionRules;
|
||||
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Retention rule interface that specify the contract for all retention rules.
|
||||
/// </summary>
|
||||
public interface IRetentionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Apply retention rule.
|
||||
/// </summary>
|
||||
/// <param name="vaults">List of existing vaults to apply the retention rule to.</param>
|
||||
/// <param name="now">Current DateTime.</param>
|
||||
/// <returns>Vaults that should be kept according to the retention rule.</returns>
|
||||
IEnumerable<Vault> ApplyRule(List<Vault> vaults, DateTime now);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MonthlyRetentionRule.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.Api.Vault.RetentionRules;
|
||||
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Monthly retention rule that keeps the latest vault for each month.
|
||||
/// </summary>
|
||||
public class MonthlyRetentionRule : IRetentionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets amount of months to keep vault.
|
||||
/// </summary>
|
||||
public int MonthsToKeep { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IRetentionRule.ApplyRule"/>
|
||||
public IEnumerable<Vault> ApplyRule(List<Vault> vaults, DateTime now)
|
||||
{
|
||||
return vaults
|
||||
.GroupBy(x => x.UpdatedAt.Month)
|
||||
.Select(g => g.OrderByDescending(x => x.UpdatedAt).First())
|
||||
.OrderByDescending(x => x.UpdatedAt)
|
||||
.Take(MonthsToKeep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="VersionRetentionRule.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.Api.Vault.RetentionRules;
|
||||
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Version retention rule that keeps the latest X unique versions of the vault.
|
||||
/// </summary>
|
||||
public class VersionRetentionRule : IRetentionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets amount of versions to keep the vault.
|
||||
/// </summary>
|
||||
public int VersionsToKeep { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IRetentionRule.ApplyRule"/>
|
||||
public IEnumerable<Vault> ApplyRule(List<Vault> vaults, DateTime now)
|
||||
{
|
||||
// For the specified amount of versions, take last vault per version.
|
||||
return vaults
|
||||
.GroupBy(x => x.Version)
|
||||
.Select(g => g.OrderByDescending(x => x.UpdatedAt).First())
|
||||
.OrderByDescending(x => x.UpdatedAt)
|
||||
.Take(VersionsToKeep);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="WeeklyRetentionRule.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.Api.Vault.RetentionRules;
|
||||
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// Weekly retention rule that keeps the latest vault for each week.
|
||||
/// </summary>
|
||||
public class WeeklyRetentionRule : IRetentionRule
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets amount of weeks to keep vault.
|
||||
/// </summary>
|
||||
public int WeeksToKeep { get; set; }
|
||||
|
||||
/// <inheritdoc cref="IRetentionRule.ApplyRule"/>
|
||||
public IEnumerable<Vault> ApplyRule(List<Vault> vaults, DateTime now)
|
||||
{
|
||||
// Helper function to get the start of the week with Monday as the first day of the week.
|
||||
DateTime GetStartOfWeek(DateTime date)
|
||||
{
|
||||
int diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
|
||||
return date.Date.AddDays(-1 * diff).Date;
|
||||
}
|
||||
|
||||
return vaults
|
||||
.GroupBy(x => GetStartOfWeek(x.UpdatedAt))
|
||||
.Select(g => g.OrderByDescending(x => x.UpdatedAt).First())
|
||||
.OrderByDescending(x => x.UpdatedAt)
|
||||
.Take(WeeksToKeep);
|
||||
}
|
||||
}
|
||||
65
src/AliasVault.Api/Vault/VaultRetentionManager.cs
Normal file
65
src/AliasVault.Api/Vault/VaultRetentionManager.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="VaultRetentionManager.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.Api.Vault;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AliasServerDb;
|
||||
|
||||
/// <summary>
|
||||
/// History manager for vaults that keeps track of vault history and applies retention rules to
|
||||
/// determine how many vaults to keep as backups and automatically deletes vaults that do no
|
||||
/// match the applied retention policies.
|
||||
/// </summary>
|
||||
public static class VaultRetentionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies retention policies to a list of existing vaults and a new vault.
|
||||
/// </summary>
|
||||
/// <param name="retentionPolicy">List of retention policies to apply.</param>
|
||||
/// <param name="existingVaults">List of existing vaults for a certain user.</param>
|
||||
/// <param name="now">DateTime which represents current time.</param>
|
||||
/// <param name="newVault">New encrypted vault to be added that is also taken into account for calculating retention policy.</param>
|
||||
/// <returns>List of vaults to delete according to the retention policies.</returns>
|
||||
public static List<Vault> ApplyRetention(RetentionPolicy retentionPolicy, List<Vault> existingVaults, DateTime now, Vault? newVault = null)
|
||||
{
|
||||
// Add the new vault to the list of existing vaults if provided
|
||||
if (newVault is not null)
|
||||
{
|
||||
existingVaults = new List<Vault>(existingVaults) { newVault };
|
||||
}
|
||||
|
||||
// Sort vaults by UpdatedAt in descending order
|
||||
existingVaults = existingVaults.OrderByDescending(v => v.UpdatedAt).ToList();
|
||||
|
||||
var vaultsToKeep = new HashSet<Vault>();
|
||||
|
||||
// Process retention rules
|
||||
foreach (var rule in retentionPolicy.Rules)
|
||||
{
|
||||
var keptVaults = rule.ApplyRule(existingVaults, now);
|
||||
foreach (var vault in keptVaults)
|
||||
{
|
||||
vaultsToKeep.Add(vault);
|
||||
}
|
||||
}
|
||||
|
||||
// Always keep the most recent vault
|
||||
if (existingVaults.Count > 0 && !vaultsToKeep.Contains(existingVaults[0]))
|
||||
{
|
||||
vaultsToKeep.Add(existingVaults[0]);
|
||||
}
|
||||
|
||||
// Determine vaults to delete
|
||||
var vaultsToDelete = existingVaults.Except(vaultsToKeep).ToList();
|
||||
|
||||
// Return the vaults to delete
|
||||
return vaultsToDelete;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,10 @@
|
||||
}
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "[[&lokl$4r<ak{f}4d#iv7>92i*)=sfo",
|
||||
"Issuer": "YourIssuer"
|
||||
"Issuer": "AliasVault"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"AliasDbContext": "Data Source=../../database/aliasdb.sqlite"
|
||||
"AliasServerDbContext": "Data Source=../../database/AliasServerDb.sqlite"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Apply database migrations using the bundle
|
||||
echo "Running database migrations..."
|
||||
/app/migrationbundle
|
||||
|
||||
# Start the application
|
||||
echo "Starting application..."
|
||||
dotnet /app/AliasVault.Api.dll
|
||||
|
||||
19
src/AliasVault.Shared/Models/FaviconExtractModel.cs
Normal file
19
src/AliasVault.Shared/Models/FaviconExtractModel.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="FaviconExtractModel.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;
|
||||
|
||||
/// <summary>
|
||||
/// FaviconExtractModel model.
|
||||
/// </summary>
|
||||
public class FaviconExtractModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets favicon image as byte array.
|
||||
/// </summary>
|
||||
public byte[]? Image { get; set; } = null!;
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Login model.
|
||||
/// </summary>
|
||||
@@ -15,10 +17,13 @@ public class LoginModel
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
namespace AliasVault.Shared.Models;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using AliasVault.Shared.Models.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Register model.
|
||||
/// </summary>
|
||||
@@ -15,20 +18,27 @@ public class RegisterModel
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string Email { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[MinLength(8, ErrorMessage = "Password must be at least 8 characters long.")]
|
||||
public string Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the password confirmation.
|
||||
/// </summary>
|
||||
[Required]
|
||||
[Compare("Password", ErrorMessage = "Passwords do not match.")]
|
||||
public string PasswordConfirm { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the terms and conditions are accepted or not.
|
||||
/// </summary>
|
||||
[MustBeTrue(ErrorMessage = "You must accept the terms and conditions.")]
|
||||
public bool AcceptTerms { get; set; } = false;
|
||||
}
|
||||
|
||||
22
src/AliasVault.Shared/Models/UnlockModel.cs
Normal file
22
src/AliasVault.Shared/Models/UnlockModel.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="UnlockModel.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;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Unlock model.
|
||||
/// </summary>
|
||||
public class UnlockModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the password.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public string Password { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="MustBeTrueAttribute.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.Validation;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
/// <summary>
|
||||
/// Validation attribute to ensure that a boolean property is true.
|
||||
/// </summary>
|
||||
public class MustBeTrueAttribute : ValidationAttribute
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool IsValid(object? value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case null:
|
||||
return false;
|
||||
case bool b:
|
||||
return b;
|
||||
default:
|
||||
throw new InvalidOperationException("Can only be used on boolean properties.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Alias.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;
|
||||
|
||||
/// <summary>
|
||||
/// Alias model.
|
||||
/// </summary>
|
||||
public class Alias
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias Service object.
|
||||
/// </summary>
|
||||
public Service Service { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias Identity object.
|
||||
/// </summary>
|
||||
public Identity Identity { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias Password object.
|
||||
/// </summary>
|
||||
public Password Password { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias CreateDate.
|
||||
/// </summary>
|
||||
public DateTime CreateDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Alias LastUpdate.
|
||||
/// </summary>
|
||||
public DateTime LastUpdate { get; set; }
|
||||
}
|
||||
42
src/AliasVault.Shared/Models/WebApi/Auth/AuthFinishModel.cs
Normal file
42
src/AliasVault.Shared/Models/WebApi/Auth/AuthFinishModel.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AuthFinishModel.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.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Auth finish model using SRP (Secure Remote Password) protocol.
|
||||
/// </summary>
|
||||
public class AuthFinishModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthFinishModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
/// <param name="a">A.</param>
|
||||
/// <param name="m1">M1.</param>
|
||||
public AuthFinishModel(string email, string a, string m1)
|
||||
{
|
||||
Email = email;
|
||||
A = a;
|
||||
M1 = m1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets A which is a value that is used to verify the user's identity.
|
||||
/// </summary>
|
||||
public string A { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets M1 which is a value that is used to verify the user's identity.
|
||||
/// </summary>
|
||||
public string M1 { get; set; }
|
||||
}
|
||||
28
src/AliasVault.Shared/Models/WebApi/Auth/AuthStartModel.cs
Normal file
28
src/AliasVault.Shared/Models/WebApi/Auth/AuthStartModel.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="AuthStartModel.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.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Auth start model using SRP (Secure Remote Password) protocol.
|
||||
/// </summary>
|
||||
public class AuthStartModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthStartModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
public AuthStartModel(string email)
|
||||
{
|
||||
this.Email = email;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
}
|
||||
28
src/AliasVault.Shared/Models/WebApi/Auth/LoginRequest.cs
Normal file
28
src/AliasVault.Shared/Models/WebApi/Auth/LoginRequest.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="LoginRequest.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.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a login request.
|
||||
/// </summary>
|
||||
public class LoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
public LoginRequest(string email)
|
||||
{
|
||||
Email = email;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email address.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
}
|
||||
39
src/AliasVault.Shared/Models/WebApi/Auth/LoginResponse.cs
Normal file
39
src/AliasVault.Shared/Models/WebApi/Auth/LoginResponse.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="LoginResponse.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.Auth;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a login response.
|
||||
/// </summary>
|
||||
public class LoginResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LoginResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="salt">Salt.</param>
|
||||
/// <param name="serverEphemeral">Server ephemeral.</param>
|
||||
public LoginResponse(string salt, string serverEphemeral)
|
||||
{
|
||||
Salt = salt;
|
||||
ServerEphemeral = serverEphemeral;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the salt.
|
||||
/// </summary>
|
||||
[JsonPropertyName("salt")]
|
||||
public string Salt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server's public ephemeral value.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverEphemeral")]
|
||||
public string ServerEphemeral { get; set; }
|
||||
}
|
||||
43
src/AliasVault.Shared/Models/WebApi/Auth/RegisterModel.cs
Normal file
43
src/AliasVault.Shared/Models/WebApi/Auth/RegisterModel.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="RegisterModel.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.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// This class represents the model for registering a new user
|
||||
/// using SRP (Secure Remote Password) protocol.
|
||||
/// </summary>
|
||||
public class RegisterModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegisterModel"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
/// <param name="salt">Salt.</param>
|
||||
/// <param name="verifier">Verifier.</param>
|
||||
public RegisterModel(string email, string salt, string verifier)
|
||||
{
|
||||
Email = email;
|
||||
Salt = salt;
|
||||
Verifier = verifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the salt.
|
||||
/// </summary>
|
||||
public string Salt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the verifier.
|
||||
/// </summary>
|
||||
public string Verifier { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ValidateLoginRequest.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.Auth
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a request to validate a login.
|
||||
/// </summary>
|
||||
public class ValidateLoginRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidateLoginRequest"/> class.
|
||||
/// </summary>
|
||||
/// <param name="email">Email.</param>
|
||||
/// <param name="clientPublicEphemeral">Client public ephemeral.</param>
|
||||
/// <param name="clientSessionProof">Client session proof.</param>
|
||||
public ValidateLoginRequest(string email, string clientPublicEphemeral, string clientSessionProof)
|
||||
{
|
||||
Email = email;
|
||||
ClientPublicEphemeral = clientPublicEphemeral;
|
||||
ClientSessionProof = clientSessionProof;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email.
|
||||
/// </summary>
|
||||
public string Email { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client's public ephemeral value.
|
||||
/// </summary>
|
||||
public string ClientPublicEphemeral { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the client's session proof.
|
||||
/// </summary>
|
||||
public string ClientSessionProof { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ValidateLoginResponse.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.Auth;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a request to validate a login.
|
||||
/// </summary>
|
||||
public class ValidateLoginResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ValidateLoginResponse"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverSessionProof">Client session proof.</param>
|
||||
/// <param name="token">Token model.</param>
|
||||
public ValidateLoginResponse(string serverSessionProof, TokenModel token)
|
||||
{
|
||||
ServerSessionProof = serverSessionProof;
|
||||
Token = token;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server's session proof.
|
||||
/// </summary>
|
||||
[JsonPropertyName("serverSessionProof")]
|
||||
public string ServerSessionProof { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JWT and refresh token.
|
||||
/// </summary>
|
||||
[JsonPropertyName("token")]
|
||||
public TokenModel Token { get; set; }
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Identity.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;
|
||||
|
||||
/// <summary>
|
||||
/// Identity model.
|
||||
/// </summary>
|
||||
public class Identity
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the identity id.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the gender.
|
||||
/// </summary>
|
||||
public string? Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the first name.
|
||||
/// </summary>
|
||||
public string? FirstName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last name.
|
||||
/// </summary>
|
||||
public string? LastName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the nickname.
|
||||
/// </summary>
|
||||
public string? NickName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the birth date.
|
||||
/// </summary>
|
||||
public string? BirthDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the street address.
|
||||
/// </summary>
|
||||
public string? AddressStreet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the city.
|
||||
/// </summary>
|
||||
public string? AddressCity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the state.
|
||||
/// </summary>
|
||||
public string? AddressState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the zip code.
|
||||
/// </summary>
|
||||
public string? AddressZipCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the country.
|
||||
/// </summary>
|
||||
public string? AddressCountry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the hobbies.
|
||||
/// </summary>
|
||||
public string? Hobbies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the email prefix.
|
||||
/// </summary>
|
||||
public string? EmailPrefix { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mobile phone number.
|
||||
/// </summary>
|
||||
public string? PhoneMobile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the bank account IBAN.
|
||||
/// </summary>
|
||||
public string? BankAccountIBAN { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time of creation.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time of last update.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default password.
|
||||
/// </summary>
|
||||
public Password? DefaultPassword { get; set; }
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Password.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;
|
||||
|
||||
/// <summary>
|
||||
/// Password model.
|
||||
/// </summary>
|
||||
public class Password
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value of the password.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the password.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the password was created.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time when the password was last updated.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="ServerValidationErrorResponse.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;
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the structure of a validation error response from the API.
|
||||
/// </summary>
|
||||
public class ServerValidationErrorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the error.
|
||||
/// </summary>
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the title of the error.
|
||||
/// </summary>
|
||||
[JsonPropertyName("title")]
|
||||
public string Title { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HTTP status code of the response.
|
||||
/// </summary>
|
||||
[JsonPropertyName("status")]
|
||||
public int Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the validation errors. The key is the name of the field that has the error, and the value is an array of error messages for that field.
|
||||
/// </summary>
|
||||
[JsonPropertyName("errors")]
|
||||
public Dictionary<string, string[]> Errors { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the trace ID of the error.
|
||||
/// </summary>
|
||||
[JsonPropertyName("traceId")]
|
||||
public string TraceId { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ServerValidationErrorResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="title">Title of the error.</param>
|
||||
/// <param name="status">Status code.</param>
|
||||
/// <returns>ServerValidationErrorResponse object.</returns>
|
||||
public static ServerValidationErrorResponse Create(string title, int status)
|
||||
{
|
||||
var errors = new Dictionary<string, string[]>
|
||||
{
|
||||
{ title, [title] },
|
||||
};
|
||||
|
||||
return new ServerValidationErrorResponse
|
||||
{
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
Title = title,
|
||||
Errors = errors,
|
||||
Status = status,
|
||||
TraceId = Guid.NewGuid().ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ServerValidationErrorResponse"/>.
|
||||
/// </summary>
|
||||
/// <param name="errorArray">Array with errors.</param>
|
||||
/// <param name="status">Status code.</param>
|
||||
/// <returns>ServerValidationErrorResponse object.</returns>
|
||||
public static ServerValidationErrorResponse Create(string[] errorArray, int status)
|
||||
{
|
||||
var errors = new Dictionary<string, string[]>();
|
||||
foreach (var t in errorArray)
|
||||
{
|
||||
errors.Add(t, new[] { t });
|
||||
}
|
||||
|
||||
return new ServerValidationErrorResponse
|
||||
{
|
||||
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
|
||||
Title = errorArray[0],
|
||||
Errors = errors,
|
||||
Status = status,
|
||||
TraceId = Guid.NewGuid().ToString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Service.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;
|
||||
|
||||
/// <summary>
|
||||
/// Service model.
|
||||
/// </summary>
|
||||
public class Service
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the service.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the description of the service.
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the service.
|
||||
/// </summary>
|
||||
public string? Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the logo URL of the service.
|
||||
/// </summary>
|
||||
public string? LogoUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the creation date and time of the service.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last updated date and time of the service.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
49
src/AliasVault.Shared/Models/WebApi/Vault.cs
Normal file
49
src/AliasVault.Shared/Models/WebApi/Vault.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="Vault.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;
|
||||
|
||||
/// <summary>
|
||||
/// Vault model.
|
||||
/// </summary>
|
||||
public class Vault
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="Vault"/> class.
|
||||
/// </summary>
|
||||
/// <param name="blob">Blob.</param>
|
||||
/// <param name="version">Version of the vault data model (migration).</param>
|
||||
/// <param name="createdAt">CreatedAt.</param>
|
||||
/// <param name="updatedAt">UpdatedAt.</param>
|
||||
public Vault(string blob, string version, DateTime createdAt, DateTime updatedAt)
|
||||
{
|
||||
Blob = blob;
|
||||
Version = version;
|
||||
CreatedAt = createdAt;
|
||||
UpdatedAt = updatedAt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vault blob.
|
||||
/// </summary>
|
||||
public string Blob { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the vault version.
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time of creation.
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date and time of last update.
|
||||
/// </summary>
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
}
|
||||
19
src/AliasVault.Shared/Providers/Time/ITimeProvider.cs
Normal file
19
src/AliasVault.Shared/Providers/Time/ITimeProvider.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="ITimeProvider.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.Providers.Time;
|
||||
|
||||
/// <summary>
|
||||
/// Time provider interface for getting the current time. We use this to be able to mock the time in tests.
|
||||
/// </summary>
|
||||
public interface ITimeProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets current time in UTC.
|
||||
/// </summary>
|
||||
DateTime UtcNow { get; }
|
||||
}
|
||||
19
src/AliasVault.Shared/Providers/Time/SystemTimeProvider.cs
Normal file
19
src/AliasVault.Shared/Providers/Time/SystemTimeProvider.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="SystemTimeProvider.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.Providers.Time;
|
||||
|
||||
/// <summary>
|
||||
/// Default time provider that uses the system clock.
|
||||
/// </summary>
|
||||
public class SystemTimeProvider : ITimeProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets current time in UTC.
|
||||
/// </summary>
|
||||
public DateTime UtcNow => DateTime.UtcNow;
|
||||
}
|
||||
39
src/AliasVault.Shared/Providers/Time/TestTimeProvider.cs
Normal file
39
src/AliasVault.Shared/Providers/Time/TestTimeProvider.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="TestTimeProvider.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.Providers.Time;
|
||||
|
||||
/// <summary>
|
||||
/// Test time provider that allows mutating the current time for testing purposes.
|
||||
/// </summary>
|
||||
public class TestTimeProvider : ITimeProvider
|
||||
{
|
||||
private DateTime _currentTime = DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Gets current time in UTC.
|
||||
/// </summary>
|
||||
public DateTime UtcNow => _currentTime;
|
||||
|
||||
/// <summary>
|
||||
/// Set the current time to a specific date and time.
|
||||
/// </summary>
|
||||
/// <param name="dateTime">DateTime to set current time to.</param>
|
||||
public void SetUtcNow(DateTime dateTime)
|
||||
{
|
||||
_currentTime = dateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Advance current time by a specific time span.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">Amount of time to advance current time by.</param>
|
||||
public void AdvanceBy(TimeSpan timeSpan)
|
||||
{
|
||||
_currentTime = _currentTime.Add(timeSpan);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>AliasVault.WebApp</RootNamespace>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<BuildVersion>$([System.DateTime]::UtcNow.ToString("yyyy-MM-dd HH:mm:ss"))</BuildVersion>
|
||||
<WasmBuildNative>true</WasmBuildNative>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DocumentationFile>bin\Debug\net8.0\AliasVault.WebApp.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<DocumentationFile>bin\Debug\net8.0\AliasVault.WebApp.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CacheBuster>dev</CacheBuster>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DocumentationFile>bin\Release\net8.0\AliasVault.WebApp.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Optimize>True</Optimize>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DocumentationFile>bin\Release\net8.0\AliasVault.WebApp.xml</DocumentationFile>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<Optimize>True</Optimize>
|
||||
<CacheBuster>$([System.DateTime]::UtcNow.ToString("yyyyMMddHHmmss"))</CacheBuster>
|
||||
</PropertyGroup>
|
||||
|
||||
<UsingTask TaskName="ReplaceText" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
|
||||
<ParameterGroup>
|
||||
<InputFile ParameterType="System.String" Required="true" />
|
||||
<OutputFile ParameterType="System.String" Required="true" />
|
||||
<CacheBuster ParameterType="System.String" Required="true" />
|
||||
<BuildVersion ParameterType="System.String" Required="true" />
|
||||
</ParameterGroup>
|
||||
<Task>
|
||||
<Code Type="Fragment" Language="cs">
|
||||
<![CDATA[
|
||||
string content = File.ReadAllText(InputFile);
|
||||
content = content.Replace("@CacheBuster", CacheBuster).Replace("@BuildVersion", BuildVersion);
|
||||
File.WriteAllText(OutputFile, content);
|
||||
Log.LogMessage(MessageImportance.High, "Replaced content in " + OutputFile);
|
||||
]]>
|
||||
</Code>
|
||||
</Task>
|
||||
</UsingTask>
|
||||
|
||||
<Target Name="GenerateCacheBustedIndexHtml" BeforeTargets="Build">
|
||||
<ReplaceText InputFile="wwwroot/index.template.html" OutputFile="wwwroot/index.html" CacheBuster="$(CacheBuster)" BuildVersion="$(BuildVersion)" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
|
||||
@@ -36,6 +63,9 @@
|
||||
<Content Update="wwwroot\appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="wwwroot\appsettings.Development.json" Condition="Exists('wwwroot\appsettings.Development.json')">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
@@ -43,19 +73,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AliasDb\AliasDb.csproj" />
|
||||
<ProjectReference Include="..\Databases\AliasClientDb\AliasClientDb.csproj" />
|
||||
<ProjectReference Include="..\AliasGenerators\AliasGenerators.csproj" />
|
||||
<ProjectReference Include="..\AliasVault.Shared\AliasVault.Shared.csproj" />
|
||||
<ProjectReference Include="..\Utilities\Cryptography\Cryptography.csproj" />
|
||||
<ProjectReference Include="..\Utilities\CsvImportExport\CsvImportExport.csproj" />
|
||||
<ProjectReference Include="..\Utilities\FaviconExtractor\FaviconExtractor.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="nginx.conf">
|
||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Pages\Aliases\Mailbox\Models\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
<div class="flex flex-col items-center justify-center px-6 pt-8 mx-auto md:h-screen pt:mt-0 dark:bg-gray-900">
|
||||
<Logo />
|
||||
<!-- Card -->
|
||||
<div class="w-full max-w-xl p-6 space-y-8 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
<div class="w-full max-w-xl p-6 space-y-4 sm:p-8 bg-white rounded-lg shadow dark:bg-gray-800">
|
||||
@Body
|
||||
</div>
|
||||
</div>
|
||||
|
||||
180
src/AliasVault.WebApp/Auth/Pages/Base/LoginBase.cs
Normal file
180
src/AliasVault.WebApp/Auth/Pages/Base/LoginBase.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="LoginBase.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.WebApp.Auth.Pages.Base;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using AliasVault.Shared.Models.WebApi;
|
||||
using AliasVault.Shared.Models.WebApi.Auth;
|
||||
using AliasVault.WebApp.Services.Auth;
|
||||
using Blazored.LocalStorage;
|
||||
using Cryptography;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
/// <summary>
|
||||
/// Base authorize page that all pages that are part of the logged in website should inherit from.
|
||||
/// All pages that inherit from this class will require the user to be logged in and have a confirmed email.
|
||||
/// Also, a default set of breadcrumbs is added in the parent OnInitialized method.
|
||||
/// </summary>
|
||||
public class LoginBase : OwningComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the NavigationManager.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public NavigationManager NavigationManager { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the HttpClient.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public HttpClient Http { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AuthenticationStateProvider.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public AuthenticationStateProvider AuthStateProvider { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the GlobalNotificationService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public GlobalNotificationService GlobalNotificationService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the IJSRuntime.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public IJSRuntime Js { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DbService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public DbService DbService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the AuthService.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public AuthService AuthService { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the LocalStorage.
|
||||
/// </summary>
|
||||
[Inject]
|
||||
public ILocalStorageService LocalStorage { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Parses the response content and displays the server validation errors.
|
||||
/// </summary>
|
||||
/// <param name="responseContent">Response content.</param>
|
||||
/// <returns>List of errors if something went wrong.</returns>
|
||||
public static List<string> ParseResponse(string responseContent)
|
||||
{
|
||||
var returnErrors = new List<string>();
|
||||
|
||||
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ServerValidationErrorResponse>(responseContent);
|
||||
if (errorResponse is not null)
|
||||
{
|
||||
foreach (var error in errorResponse.Errors)
|
||||
{
|
||||
returnErrors.AddRange(error.Value);
|
||||
}
|
||||
}
|
||||
|
||||
return returnErrors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the username from the authentication state asynchronously.
|
||||
/// </summary>
|
||||
/// <param name="email">Email address.</param>
|
||||
/// <param name="password">Password.</param>
|
||||
/// <returns>List of errors if something went wrong.</returns>
|
||||
protected async Task<List<string>> ProcessLoginAsync(string email, string password)
|
||||
{
|
||||
// Send request to server with email to get server ephemeral public key.
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/login", new LoginRequest(email));
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
return ParseResponse(responseContent);
|
||||
}
|
||||
|
||||
var loginResponse = JsonSerializer.Deserialize<LoginResponse>(responseContent);
|
||||
if (loginResponse == null)
|
||||
{
|
||||
return
|
||||
[
|
||||
"An error occurred while processing the login request.",
|
||||
];
|
||||
}
|
||||
|
||||
// 3. Client derives shared session key.
|
||||
byte[] passwordHash = await Encryption.DeriveKeyFromPasswordAsync(password, loginResponse.Salt);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
|
||||
var clientEphemeral = Srp.GenerateEphemeralClient();
|
||||
var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, email, passwordHashString);
|
||||
var clientSession = Srp.DeriveSessionClient(
|
||||
privateKey,
|
||||
clientEphemeral.Secret,
|
||||
loginResponse.ServerEphemeral,
|
||||
loginResponse.Salt,
|
||||
email);
|
||||
|
||||
// 4. Client sends proof of session key to server.
|
||||
result = await Http.PostAsJsonAsync("api/v1/Auth/validate", new ValidateLoginRequest(email, clientEphemeral.Public, clientSession.Proof));
|
||||
responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
return ParseResponse(responseContent);
|
||||
}
|
||||
|
||||
var validateLoginResponse = JsonSerializer.Deserialize<ValidateLoginResponse>(responseContent);
|
||||
if (validateLoginResponse == null)
|
||||
{
|
||||
return
|
||||
[
|
||||
"An error occurred while processing the login request.",
|
||||
];
|
||||
}
|
||||
|
||||
// 5. Client verifies proof.
|
||||
Srp.VerifySession(clientEphemeral.Public, clientSession, validateLoginResponse.ServerSessionProof);
|
||||
|
||||
// Store the tokens in local storage.
|
||||
await AuthService.StoreAccessTokenAsync(validateLoginResponse.Token.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(validateLoginResponse.Token.RefreshToken);
|
||||
|
||||
// Store the encryption key in memory.
|
||||
AuthService.StoreEncryptionKey(passwordHash);
|
||||
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Redirect to the page the user was trying to access before if set.
|
||||
var localStorageReturnUrl = await LocalStorage.GetItemAsync<string>("returnUrl");
|
||||
if (!string.IsNullOrEmpty(localStorageReturnUrl))
|
||||
{
|
||||
NavigationManager.NavigateTo(localStorageReturnUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
@page "/user/login"
|
||||
@attribute [AllowAnonymous]
|
||||
@inherits AliasVault.WebApp.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.MainLayout
|
||||
|
||||
@inject HttpClient Http
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthService AuthService
|
||||
@using System.Text.Json
|
||||
@attribute [AllowAnonymous]
|
||||
@using AliasVault.Shared.Models
|
||||
@using AliasVault.WebApp.Auth.Components
|
||||
@using AliasVault.WebApp.Auth.Services
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Sign in to AliasVault
|
||||
</h2>
|
||||
|
||||
<EditForm Model="_user" OnSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<EditForm Model="LoginModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<InputTextField id="email" @bind-Value="_user.Email" placeholder="name@company.com" />
|
||||
<InputTextField id="email" @bind-Value="LoginModel.Email" placeholder="name@company.com" />
|
||||
<ValidationMessage For="() => LoginModel.Email"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="_user.Password" type="password" placeholder="••••••••" />
|
||||
<InputTextField id="password" @bind-Value="LoginModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => LoginModel.Password"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
@@ -41,12 +41,10 @@
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
|
||||
<FullScreenLoadingIndicator @ref="_loadingIndicator" />
|
||||
|
||||
@code {
|
||||
private LoginModel _user = new LoginModel();
|
||||
private FullScreenLoadingIndicator _loadingIndicator = new();
|
||||
private readonly LoginModel LoginModel = new();
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private ServerValidationErrors ServerValidationErrors = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -61,37 +59,33 @@
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
_loadingIndicator.Show();
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Http.PostAsJsonAsync("api/Auth/login", _user);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject != null)
|
||||
var errors = await ProcessLoginAsync(LoginModel.Email, LoginModel.Password);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
// Store the token as a plain string in local storage
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle the case where the token is not present in the response
|
||||
Console.WriteLine("Token not found in the response.");
|
||||
}
|
||||
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
|
||||
if (result.IsSuccessStatusCode)
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
ServerValidationErrors.AddError(error);
|
||||
}
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If in debug mode show the actual exception.
|
||||
ServerValidationErrors.AddError(ex.ToString());
|
||||
}
|
||||
#else
|
||||
catch
|
||||
{
|
||||
// If in release mode show a generic error.
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
}
|
||||
#endif
|
||||
finally
|
||||
{
|
||||
_loadingIndicator.Hide();
|
||||
LoadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@page "/user/logout"
|
||||
@using AliasVault.WebApp.Auth.Services
|
||||
@attribute [AllowAnonymous]
|
||||
@layout Auth.Layout.MainLayout
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject AuthService AuthService
|
||||
@inject GlobalNotificationService GlobalNotificationService
|
||||
@inject DbService DbService
|
||||
|
||||
@code {
|
||||
/// <inheritdoc />
|
||||
@@ -13,6 +14,9 @@
|
||||
await base.OnInitializedAsync();
|
||||
await AuthService.RemoveTokensAsync();
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
// Initialize a new empty database to clear all data.
|
||||
DbService.InitializeEmptyDatabase();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
|
||||
// Redirect to home page
|
||||
NavigationManager.NavigateTo("/");
|
||||
|
||||
@@ -8,32 +8,42 @@
|
||||
@using System.Text.Json
|
||||
@using AliasVault.Shared.Models
|
||||
@using AliasVault.WebApp.Auth.Components
|
||||
@using AliasVault.WebApp.Auth.Services
|
||||
@using AliasVault.WebApp.Auth.Pages.Base
|
||||
@using Cryptography
|
||||
@using SecureRemotePassword
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
Create a Free Account
|
||||
Create a new AliasVault account
|
||||
</h2>
|
||||
|
||||
<EditForm Model="user" OnSubmit="HandleRegister" class="mt-8 space-y-6">
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<EditForm Model="RegisterModel" OnValidSubmit="HandleRegister" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Email" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your email</label>
|
||||
<InputTextField id="email" @bind-Value="user.Email" placeholder="name@company.com" />
|
||||
<InputTextField id="email" @bind-Value="RegisterModel.Email" placeholder="name@company.com" />
|
||||
<ValidationMessage For="() => RegisterModel.Email"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="user.Password" type="password" placeholder="••••••••" />
|
||||
<InputTextField id="password" @bind-Value="RegisterModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => RegisterModel.Password"/>
|
||||
</div>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Confirm password</label>
|
||||
<InputTextField id="password2" @bind-Value="user.PasswordConfirm" type="password" placeholder="••••••••" />
|
||||
<InputTextField id="password2" @bind-Value="RegisterModel.PasswordConfirm" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => RegisterModel.PasswordConfirm"/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start">
|
||||
<div class="flex items-center h-5">
|
||||
<input id="terms" aria-describedby="terms" name="terms" type="checkbox" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" required>
|
||||
<InputCheckbox id="terms" @bind-Value="RegisterModel.AcceptTerms" class="w-4 h-4 border-gray-300 rounded bg-gray-50 focus:ring-3 focus:ring-primary-300 dark:focus:ring-primary-600 dark:ring-offset-gray-800 dark:bg-gray-700 dark:border-gray-600" />
|
||||
</div>
|
||||
<div class="ml-3 text-sm">
|
||||
<label for="terms" class="font-medium text-gray-900 dark:text-white">I accept the <a href="#" class="text-primary-700 hover:underline dark:text-primary-500">Terms and Conditions</a></label>
|
||||
<ValidationMessage For="() => RegisterModel.AcceptTerms"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -43,79 +53,61 @@
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (validationErrors.Any())
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@foreach (var error in validationErrors)
|
||||
{
|
||||
<li>@error</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<FullScreenLoadingIndicator @ref="loadingIndicator" />
|
||||
|
||||
@code {
|
||||
RegisterModel user = new();
|
||||
FullScreenLoadingIndicator loadingIndicator = new();
|
||||
List<string> validationErrors = [];
|
||||
private readonly RegisterModel RegisterModel = new();
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private ServerValidationErrors ServerValidationErrors = new();
|
||||
|
||||
async Task HandleRegister()
|
||||
{
|
||||
loadingIndicator.Show();
|
||||
validationErrors.Clear();
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Http.PostAsJsonAsync("api/Auth/register", user);
|
||||
var client = new SrpClient();
|
||||
var salt = client.GenerateSalt();
|
||||
|
||||
if (result.IsSuccessStatusCode)
|
||||
byte[] passwordHash = await Encryption.DeriveKeyFromPasswordAsync(RegisterModel.Password, salt);
|
||||
var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty);
|
||||
|
||||
var srpSignup = Cryptography.Srp.SignupPrepareAsync(client, salt, RegisterModel.Email, passwordHashString);
|
||||
var result = await Http.PostAsJsonAsync("api/v1/Auth/register", srpSignup);
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
|
||||
if (!result.IsSuccessStatusCode)
|
||||
{
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject != null)
|
||||
foreach (var error in LoginBase.ParseResponse(responseContent))
|
||||
{
|
||||
// Store the token as a plain string in local storage
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle the case where the token is not present in the response
|
||||
Console.WriteLine("Token not found in the response.");
|
||||
ServerValidationErrors.AddError(error);
|
||||
}
|
||||
StateHasChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/");
|
||||
var tokenObject = JsonSerializer.Deserialize<TokenModel>(responseContent);
|
||||
|
||||
if (tokenObject != null)
|
||||
{
|
||||
// Store the encryption key in memory.
|
||||
AuthService.StoreEncryptionKey(passwordHash);
|
||||
|
||||
// Store the token as a plain string in local storage
|
||||
await AuthService.StoreAccessTokenAsync(tokenObject.Token);
|
||||
await AuthService.StoreRefreshTokenAsync(tokenObject.RefreshToken);
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
var responseContent = await result.Content.ReadAsStringAsync();
|
||||
var errorResponse = System.Text.Json.JsonSerializer.Deserialize<ValidationErrorResponse>(responseContent);
|
||||
if (errorResponse != null && errorResponse.Errors != null)
|
||||
{
|
||||
foreach (var error in errorResponse.Errors.Values)
|
||||
{
|
||||
validationErrors.AddRange(error);
|
||||
}
|
||||
}
|
||||
// Handle the case where the token is not present in the response
|
||||
Console.WriteLine("Token not found in the response.");
|
||||
}
|
||||
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
finally
|
||||
{
|
||||
loadingIndicator.Hide();
|
||||
LoadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidationErrorResponse
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public string Title { get; set; } = null!;
|
||||
public int Status { get; set; }
|
||||
public Dictionary<string, string[]> Errors { get; set; } = new();
|
||||
public string TraceId { get; set; } = null!;
|
||||
}
|
||||
}
|
||||
|
||||
103
src/AliasVault.WebApp/Auth/Pages/Unlock.razor
Normal file
103
src/AliasVault.WebApp/Auth/Pages/Unlock.razor
Normal file
@@ -0,0 +1,103 @@
|
||||
@page "/unlock"
|
||||
@inherits AliasVault.WebApp.Auth.Pages.Base.LoginBase
|
||||
@layout Auth.Layout.MainLayout
|
||||
@using AliasVault.Shared.Models
|
||||
@using AliasVault.WebApp.Auth.Components
|
||||
|
||||
<div class="flex space-x-4">
|
||||
<img class="w-8 h-8 rounded-full" src="/img/avatar.webp" alt="Bonnie image">
|
||||
<h2 class="mb-3 text-2xl font-bold text-gray-900 dark:text-white">@Email</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-base font-normal text-gray-500 dark:text-gray-400">
|
||||
Enter your master password in order to unlock your database.
|
||||
</p>
|
||||
|
||||
<FullScreenLoadingIndicator @ref="LoadingIndicator" />
|
||||
<ServerValidationErrors @ref="ServerValidationErrors" />
|
||||
|
||||
<EditForm Model="UnlockModel" OnValidSubmit="HandleLogin" class="mt-8 space-y-6">
|
||||
<DataAnnotationsValidator/>
|
||||
<div>
|
||||
<label asp-for="Input.Password" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Your password</label>
|
||||
<InputTextField id="password" @bind-Value="UnlockModel.Password" type="password" placeholder="••••••••" />
|
||||
<ValidationMessage For="() => UnlockModel.Password"/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="inline-flex items-center justify-center w-full px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-primary-700 hover:bg-primary-800 focus:ring-4 focus:ring-primary-300 sm:w-auto dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800">
|
||||
<svg class="w-5 h-5 mr-2 -ml-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10 2a5 5 0 00-5 5v2a2 2 0 00-2 2v5a2 2 0 002 2h10a2 2 0 002-2v-5a2 2 0 00-2-2H7V7a3 3 0 015.905-.75 1 1 0 001.937-.5A5.002 5.002 0 0010 2z"></path></svg>
|
||||
Unlock
|
||||
</button>
|
||||
|
||||
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Switch accounts? <a href="/user/logout" class="text-primary-700 hover:underline dark:text-primary-500">Logout</a>
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
private string? Email { get; set; }
|
||||
private readonly UnlockModel UnlockModel = new();
|
||||
private FullScreenLoadingIndicator LoadingIndicator = new();
|
||||
private ServerValidationErrors ServerValidationErrors = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
if (authState.User.Identity?.IsAuthenticated == false) {
|
||||
// Not authenticated (anymore), redirect to login page.
|
||||
NavigationManager.NavigateTo("/user/login");
|
||||
}
|
||||
|
||||
var email = authState.User.Identity?.Name;
|
||||
if (email is null)
|
||||
{
|
||||
// Clear all tokens and redirect to login page.
|
||||
await AuthService.RemoveTokensAsync();
|
||||
await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
GlobalNotificationService.ClearMessages();
|
||||
NavigationManager.NavigateTo("/user/login");
|
||||
}
|
||||
|
||||
Email = email;
|
||||
}
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
if (Email == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LoadingIndicator.Show();
|
||||
ServerValidationErrors.Clear();
|
||||
|
||||
try
|
||||
{
|
||||
var errors = await ProcessLoginAsync(Email, UnlockModel.Password);
|
||||
foreach (var error in errors)
|
||||
{
|
||||
ServerValidationErrors.AddError(error);
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
#if DEBUG
|
||||
catch (Exception ex)
|
||||
{
|
||||
// If in debug mode show the actual exception.
|
||||
ServerValidationErrors.AddError(ex.ToString());
|
||||
}
|
||||
#else
|
||||
catch
|
||||
{
|
||||
// If in release mode show a generic error.
|
||||
ServerValidationErrors.AddError("An error occurred while processing the login request. Try again (later).");
|
||||
}
|
||||
#endif
|
||||
finally
|
||||
{
|
||||
LoadingIndicator.Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
@if (faviconBytes != null)
|
||||
{
|
||||
<img src="@faviconDataUrl" style="width: 50px;" alt="Favicon" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="img/service-placeholder.webp" style="width: 50px;" alt="Favicon" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public byte[]? faviconBytes { get; set; }
|
||||
|
||||
private string? faviconDataUrl;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (faviconBytes != null)
|
||||
{
|
||||
string base64String = Convert.ToBase64String(faviconBytes);
|
||||
faviconDataUrl = $"data:image/x-icon;base64,{base64String}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
@using AliasVault.WebApp.Pages.Aliases.Models.Spamok
|
||||
@using BlazorServer.Models.Spamok
|
||||
@inherits ComponentBase
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
|
||||
<div class="flex justify-between">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
|
||||
<button @onclick="LoadRecentEmailsAsync" type="button" class="text-blue-700 border border-blue-700 hover:bg-blue-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-blue-500">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else if (MailboxEmails.Count == 0)
|
||||
{
|
||||
<div>No emails found.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex flex-col mt-6">
|
||||
<div class="overflow-x-auto rounded-lg">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div class="overflow-hidden shadow sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
@foreach (var mail in MailboxEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<a target="_blank" href="https://spamok.com/@mail.ToLocal/@mail.Id">@(mail.Subject.Substring(0,mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</a>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<a target="_blank" href="https://spamok.com/@mail.ToLocal/@mail.Id">@mail.DateSystem</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string EmailPrefix { get; set; } = string.Empty;
|
||||
[Parameter]
|
||||
public List<MailboxEmailApiModel> MailboxEmails { get; set; } = new List<MailboxEmailApiModel>();
|
||||
public bool IsLoading { get; set; } = true;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadRecentEmailsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadRecentEmailsAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
MailboxApiModel? mailbox = await client.GetFromJsonAsync<MailboxApiModel>($"https://api.spamok.com/v2/EmailBox/{EmailPrefix}");
|
||||
|
||||
if (mailbox?.Mails != null)
|
||||
{
|
||||
MailboxEmails = mailbox.Mails;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,16 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
|
||||
# Install Python which is required by the WebAssembly tools
|
||||
RUN apt-get update && apt-get install -y python3 && apt-get clean
|
||||
|
||||
# Install the WebAssembly tools
|
||||
RUN dotnet workload install wasm-tools
|
||||
|
||||
# Copy the project files and restore dependencies
|
||||
COPY ["src/AliasVault.WebApp/AliasVault.WebApp.csproj", "src/AliasVault.WebApp/"]
|
||||
COPY ["src/AliasVault.Shared/AliasVault.Shared.csproj", "src/AliasVault.Shared/"]
|
||||
COPY ["src/AliasDb/AliasDb.csproj", "src/AliasDb/"]
|
||||
COPY ["src/Databases/AliasServerDb/AliasServerDb.csproj", "src/Databases/AliasServerDb/"]
|
||||
COPY ["src/AliasGenerators/AliasGenerators.csproj", "src/AliasGenerators/"]
|
||||
COPY ["src/Utilities/FaviconExtractor/FaviconExtractor.csproj", "src/Utilities/FaviconExtractor/"]
|
||||
RUN dotnet restore "src/AliasVault.WebApp/AliasVault.WebApp.csproj"
|
||||
@@ -20,12 +26,12 @@ COPY . .
|
||||
|
||||
# Build the WebApp project
|
||||
WORKDIR "/src/src/AliasVault.WebApp"
|
||||
RUN dotnet build "AliasVault.WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
RUN dotnet build "AliasVault.WebApp.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
|
||||
|
||||
# Publish the WebApp project
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "AliasVault.WebApp.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "AliasVault.WebApp.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Final stage: start nginx and serve static html files that were published in the previous stage
|
||||
FROM nginx:alpine AS final
|
||||
|
||||
15
src/AliasVault.WebApp/GlobalUsings.cs
Normal file
15
src/AliasVault.WebApp/GlobalUsings.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
// -----------------------------------------------------------------------
|
||||
// <copyright file="GlobalUsings.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>
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// <auto-generated/>
|
||||
// Note: this file is not actually auto-generated but the statement above is required because
|
||||
// otherwise a lot of SA1200 checks will fail due to it not fully supporting GlobalUsings at time of writing.
|
||||
|
||||
global using AliasVault.WebApp.Main.Models;
|
||||
global using AliasVault.WebApp.Services;
|
||||
global using AliasVault.WebApp.Services.Auth;
|
||||
global using AliasVault.WebApp.Services.Database;
|
||||
@@ -1,14 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<TopMenu />
|
||||
|
||||
<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>
|
||||
@Body
|
||||
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,77 +0,0 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="">AliasVault</a>
|
||||
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="counter">
|
||||
<span class="oi oi-plus" aria-hidden="true"></span> Counter
|
||||
</NavLink>
|
||||
</div>
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="fetchdata">
|
||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool collapseNavMenu = true;
|
||||
|
||||
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
|
||||
|
||||
private void ToggleNavMenu()
|
||||
{
|
||||
collapseNavMenu = !collapseNavMenu;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@inherits ComponentBase
|
||||
@inherits ComponentBase
|
||||
|
||||
@if (Message.IsNullOrEmpty())
|
||||
@if (Message == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -11,6 +10,9 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The message to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
@inherits ComponentBase
|
||||
@inherits ComponentBase
|
||||
|
||||
@if (Message.IsNullOrEmpty())
|
||||
@if (Message == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -11,7 +10,9 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The message to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Message { get; set; } = string.Empty;
|
||||
|
||||
}
|
||||
@@ -48,20 +48,20 @@
|
||||
/// <summary>
|
||||
/// Refreshes the messages by adding any new messages from the PortalMessageService.
|
||||
/// </summary>
|
||||
public void RefreshAddMessages()
|
||||
private void RefreshAddMessages()
|
||||
{
|
||||
// We retrieve any additional messages from the GlobalNotificationService that we do not yet have.
|
||||
var newMessages = GlobalNotificationService.GetMessagesForDisplay();
|
||||
foreach (var message in newMessages)
|
||||
{
|
||||
if (!Messages.Any(m => m.Key == message.Key && m.Value == message.Value))
|
||||
if (!Messages.Exists(m => m.Key == message.Key && m.Value == message.Value))
|
||||
{
|
||||
Messages.Add(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove messages that are no longer in the GlobalNotificationService and have already been displayed.
|
||||
var messagesToRemove = Messages.Where(m => !newMessages.Any(nm => nm.Key == m.Key && nm.Value == m.Value)).ToList();
|
||||
var messagesToRemove = Messages.Where(m => !newMessages.Exists(nm => nm.Key == m.Key && nm.Value == m.Value)).ToList();
|
||||
foreach (var message in messagesToRemove)
|
||||
{
|
||||
Messages.Remove(message);
|
||||
@@ -0,0 +1,31 @@
|
||||
@using AliasVault.Shared.Models.WebApi
|
||||
|
||||
@if (_errors.Any())
|
||||
{
|
||||
@foreach (var error in _errors)
|
||||
{
|
||||
<AlertMessageError Message="@error" />
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private readonly List<string> _errors = [];
|
||||
|
||||
/// <summary>
|
||||
/// Adds a server validation error.
|
||||
/// </summary>
|
||||
public void AddError(string error)
|
||||
{
|
||||
_errors.Add(error);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the server validation errors.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_errors.Clear();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@using System.IO
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<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">Attachments</h3>
|
||||
<div class="grid gap-6">
|
||||
<div class="col-span-6 sm:col-span-3">
|
||||
<InputFile OnChange="@HandleFileSelection" class="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 dark:text-gray-400 focus:outline-none dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400" />
|
||||
@if (!string.IsNullOrEmpty(statusMessage))
|
||||
{
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">@statusMessage</p>
|
||||
}
|
||||
@if (Attachments.Any())
|
||||
{
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-2 text-lg font-semibold dark:text-white">Attachments:</h4>
|
||||
<ul class="list-disc list-inside">
|
||||
@foreach (var attachment in Attachments)
|
||||
{
|
||||
<li class="flex items-center justify-between text-sm text-gray-600 dark:text-gray-400">
|
||||
<span>@attachment.Filename</span>
|
||||
<button type="button" @onclick="() => DeleteAttachment(attachment)" class="text-red-500 hover:text-red-700">
|
||||
Delete
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public List<Attachment> Attachments { get; set; } = new List<Attachment>();
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<List<Attachment>> AttachmentsChanged { get; set; }
|
||||
|
||||
private string statusMessage = string.Empty;
|
||||
|
||||
private async Task HandleFileSelection(InputFileChangeEventArgs e)
|
||||
{
|
||||
statusMessage = "Uploading...";
|
||||
foreach (var file in e.GetMultipleFiles(int.MaxValue))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
await file.OpenReadStream().CopyToAsync(ms);
|
||||
|
||||
var attachment = new Attachment
|
||||
{
|
||||
Filename = file.Name,
|
||||
Blob = ms.ToArray(),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
Attachments.Add(attachment);
|
||||
await AttachmentsChanged.InvokeAsync(Attachments);
|
||||
|
||||
statusMessage = "File uploaded successfully.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusMessage = $"Error uploading file: {ex.Message}";
|
||||
await JSRuntime.InvokeVoidAsync("console.error", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private async Task DeleteAttachment(Attachment attachment)
|
||||
{
|
||||
try
|
||||
{
|
||||
Attachments.Remove(attachment);
|
||||
await AttachmentsChanged.InvokeAsync(Attachments);
|
||||
|
||||
statusMessage = "Attachment deleted successfully.";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
statusMessage = $"Error deleting attachment: {ex.Message}";
|
||||
await JSRuntime.InvokeVoidAsync("console.error", ex.Message);
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<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">Attachments</h3>
|
||||
@if (Attachments.Any())
|
||||
{
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">Filename</th>
|
||||
<th scope="col" class="px-6 py-3">Created At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var attachment in Attachments)
|
||||
{
|
||||
<tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
|
||||
<td class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<span @onclick="() => DownloadAttachment(attachment)" class="text-primary cursor-pointer">@attachment.Filename</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
@attachment.CreatedAt.ToLocalTime().ToString("g")
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-gray-500 dark:text-gray-400">No attachments available.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public ICollection<Attachment> Attachments { get; set; } = new List<Attachment>();
|
||||
|
||||
private async Task DownloadAttachment(Attachment attachment)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (attachment.Blob != null)
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("downloadFileFromStream", attachment.Filename, attachment.Blob);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle the case where the attachment or its content is not found
|
||||
await Console.Error.WriteLineAsync($"Attachment not found or has no content: {attachment.Id}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle any exceptions that occur during the download process
|
||||
await Console.Error.WriteLineAsync($"Error downloading attachment: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div @onclick="ShowDetails" class="p-4 space-y-2 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700
|
||||
<div @onclick="ShowDetails" class="credential-card p-4 space-y-2 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700
|
||||
dark:bg-gray-800 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors duration-200">
|
||||
<div class="px-4 py-2 text-gray-400 rounded text-center flex flex-col items-center">
|
||||
<DisplayFavicon faviconBytes="null"></DisplayFavicon>
|
||||
<DisplayFavicon faviconBytes="@Obj.Logo"></DisplayFavicon>
|
||||
<div>@Obj.Service</div>
|
||||
<div>@Obj.CreateDate.ToShortDateString()</div>
|
||||
</div>
|
||||
@@ -11,14 +11,14 @@
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Gets or sets the alias object to show.
|
||||
/// Gets or sets the credentials object to show.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public AliasVault.Shared.Models.WebApi.AliasListEntry Obj { get; set; } = null!;
|
||||
public CredentialListEntry Obj { get; set; } = null!;
|
||||
|
||||
private void ShowDetails()
|
||||
{
|
||||
// Redirect to view page instead for now.
|
||||
NavigationManager.NavigateTo($"/alias/{Obj.Id}");
|
||||
NavigationManager.NavigateTo($"/credentials/{Obj.Id}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
@if (FaviconBytes != null)
|
||||
{
|
||||
<img src="@_faviconDataUrl" style="width: 50px;" class="mb-4 rounded-lg w-28 sm:mb-0 xl:mb-4 2xl:mb-0" alt="Favicon" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="img/service-placeholder.webp" style="width: 50px;" alt="Favicon" />
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Byte[] of the favicon.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public byte[]? FaviconBytes { get; set; }
|
||||
|
||||
private string? _faviconDataUrl;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (FaviconBytes is not null)
|
||||
{
|
||||
string base64String = Convert.ToBase64String(FaviconBytes);
|
||||
_faviconDataUrl = $"data:image/x-icon;base64,{base64String}";
|
||||
}
|
||||
}
|
||||
}
|
||||
125
src/AliasVault.WebApp/Main/Components/Email/RecentEmails.razor
Normal file
125
src/AliasVault.WebApp/Main/Components/Email/RecentEmails.razor
Normal file
@@ -0,0 +1,125 @@
|
||||
@using AliasVault.WebApp.Main.Models.Spamok
|
||||
@inherits ComponentBase
|
||||
@inject IHttpClientFactory HttpClientFactory
|
||||
|
||||
@if (ShowComponent)
|
||||
{
|
||||
<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="flex justify-between">
|
||||
<h3 class="mb-4 text-xl font-semibold dark:text-white">Email</h3>
|
||||
<button @onclick="LoadRecentEmailsAsync" type="button" class="text-blue-700 border border-blue-700 hover:bg-blue-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center dark:border-blue-500 dark:text-blue-500 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-blue-500">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<LoadingIndicator/>
|
||||
}
|
||||
else if (MailboxEmails.Count == 0)
|
||||
{
|
||||
<div>No emails found.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex flex-col mt-6">
|
||||
<div class="overflow-x-auto rounded-lg">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<div class="overflow-hidden shadow sm:rounded-lg">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Subject
|
||||
</th>
|
||||
<th scope="col" class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white">
|
||||
Date & Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800">
|
||||
@foreach (var mail in MailboxEmails)
|
||||
{
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||
<a target="_blank" href="https://spamok.com/@mail.ToLocal/@mail.Id">@(mail.Subject.Substring(0, mail.Subject.Length > 30 ? 30 : mail.Subject.Length))...</a>
|
||||
</td>
|
||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||
<a target="_blank" href="https://spamok.com/@mail.ToLocal/@mail.Id">@mail.DateSystem</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// The email address to show recent emails for.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
private List<MailboxEmailApiModel> MailboxEmails { get; set; } = new();
|
||||
private bool IsLoading { get; set; } = true;
|
||||
private bool ShowComponent { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
// Check if email has a known SpamOK domain, if not, don't show this component.
|
||||
if (Email.EndsWith("@landmail.nl"))
|
||||
{
|
||||
ShowComponent = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
await base.OnAfterRenderAsync(firstRender);
|
||||
|
||||
if (!ShowComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
await LoadRecentEmailsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadRecentEmailsAsync()
|
||||
{
|
||||
if (!ShowComponent)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
StateHasChanged();
|
||||
|
||||
// Get email prefix, which is the part before the @ symbol.
|
||||
string emailPrefix = Email.Split('@')[0];
|
||||
|
||||
var client = HttpClientFactory.CreateClient("EmailClient");
|
||||
MailboxApiModel? mailbox = await client.GetFromJsonAsync<MailboxApiModel>($"https://api.spamok.com/v2/EmailBox/{emailPrefix}");
|
||||
|
||||
if (mailbox?.Mails != null)
|
||||
{
|
||||
MailboxEmails = mailbox.Mails;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@using AliasVault.WebApp.Services
|
||||
|
||||
<!-- CopyPasteFormRow.razor -->
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject IJSRuntime JsRuntime
|
||||
@@ -20,7 +21,7 @@
|
||||
[Parameter] public string Value { get; set; } = string.Empty;
|
||||
|
||||
private bool _copied => ClipboardCopyService.GetCopiedId() == _inputId;
|
||||
private string _inputId = Guid.NewGuid().ToString();
|
||||
private readonly string _inputId = Guid.NewGuid().ToString();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -1,10 +1,16 @@
|
||||
@using AliasVault.WebApp.Services
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject ClipboardCopyService ClipboardCopyService
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<label for="@Id" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">@Label</label>
|
||||
<div class="relative">
|
||||
<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">
|
||||
@if (Type == "textarea")
|
||||
{
|
||||
<textarea id="@Id" style="height: 200px;" 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"></textarea>
|
||||
}
|
||||
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">
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
@@ -20,6 +26,12 @@
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "Value";
|
||||
|
||||
/// <summary>
|
||||
/// Type of input field.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Type { get; set; } = "text";
|
||||
|
||||
/// <summary>
|
||||
/// Value of the input field.
|
||||
/// </summary>
|
||||
@@ -1,7 +1,6 @@
|
||||
@inherits ComponentBase
|
||||
@using Microsoft.IdentityModel.Tokens
|
||||
|
||||
<nav class="flex mb-5" aria-label="RecentEmails">
|
||||
<nav class="flex mb-5">
|
||||
<ol class="inline-flex items-center space-x-1 text-sm font-medium md:space-x-2">
|
||||
<li class="inline-flex items-center">
|
||||
<a href="/" class="inline-flex items-center text-gray-700 hover:text-primary-600 dark:text-gray-300 dark:hover:text-primary-500">
|
||||
@@ -11,7 +10,7 @@
|
||||
</li>
|
||||
@foreach (var item in BreadcrumbItems)
|
||||
{
|
||||
@if (!item.Url.IsNullOrEmpty())
|
||||
@if (item.Url is not null)
|
||||
{
|
||||
<li>
|
||||
<div class="flex items-center">
|
||||
@@ -46,7 +45,7 @@
|
||||
{
|
||||
base.OnInitialized();
|
||||
// Remove first item if it is the home page
|
||||
if (BreadcrumbItems.Any() && BreadcrumbItems.First().DisplayName == "Home")
|
||||
if (BreadcrumbItems.Any() && BreadcrumbItems[0].DisplayName == "Home")
|
||||
{
|
||||
BreadcrumbItems.RemoveAt(0);
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
<div class="loading" style="display:@(IsVisible ? "block" : "none");">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
@if (IsVisible)
|
||||
{
|
||||
<div class="loading z-50">
|
||||
<div class="spinner">
|
||||
<div class="rect1"></div>
|
||||
<div class="rect2"></div>
|
||||
<div class="rect3"></div>
|
||||
<div class="rect4"></div>
|
||||
<div class="rect5"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool IsVisible { get; set; }
|
||||
@@ -0,0 +1,22 @@
|
||||
<div role="status" class="px-2" title="@Title">
|
||||
<svg aria-hidden="true" class="inline w-7 h-7 text-gray-200 @(Spinning ? "animate-spin fill-primary-600" : "") dark:text-gray-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Optional title of the loading indicator.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Set spinning to false to stop the animation.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Spinning { get; set; } = true;
|
||||
|
||||
}
|
||||
63
src/AliasVault.WebApp/Main/Layout/DbStatusIndicator.razor
Normal file
63
src/AliasVault.WebApp/Main/Layout/DbStatusIndicator.razor
Normal file
@@ -0,0 +1,63 @@
|
||||
@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>
|
||||
-->
|
||||
|
||||
@code {
|
||||
private bool Loading { get; set; } = false;
|
||||
private string Message { get; set; } = "";
|
||||
private string LoadingIndicatorMessage { get; set; } = "";
|
||||
private bool DatabaseLoading { get; set; } = false;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
DbService.GetState().StateChanged += OnDatabaseStateChanged;
|
||||
}
|
||||
|
||||
private async void OnDatabaseStateChanged(object? sender, DbServiceState.DatabaseState newState)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
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 async Task ShowLoadingIndicatorAsync()
|
||||
{
|
||||
Loading = true;
|
||||
StateHasChanged();
|
||||
await Task.Delay(800);
|
||||
Loading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose method.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
DbService.GetState().StateChanged -= OnDatabaseStateChanged;
|
||||
}
|
||||
}
|
||||
16
src/AliasVault.WebApp/Main/Layout/EmptyLayout.razor
Normal file
16
src/AliasVault.WebApp/Main/Layout/EmptyLayout.razor
Normal file
@@ -0,0 +1,16 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<CascadingAuthenticationState>
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<main>
|
||||
@Body
|
||||
</main>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
</CascadingAuthenticationState>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user