Compare commits

...

128 Commits

Author SHA1 Message Date
Leendert de Borst
ac64dba715 Merge pull request #509 from lanedirt/508-prepare-0100-release
Prepare 0.10.0 release
2025-01-01 15:43:28 +01:00
Leendert de Borst
d2f9b225d0 Start services again after db migration (#508) 2025-01-01 15:01:13 +01:00
Leendert de Borst
d7f1df3252 Update AppInfo bump version (#508) 2025-01-01 14:56:28 +01:00
Leendert de Borst
fdce8bddd1 Bump install.sh version (#508) 2025-01-01 14:56:15 +01:00
Leendert de Borst
be4a105709 Update docs (#508) 2025-01-01 14:56:02 +01:00
Leendert de Borst
dfa2f84570 Merge pull request #507 from lanedirt/506-add-postgresql-database-import-and-export-commands-to-installsh
Add postgresql database import and export commands to install.sh
2024-12-31 21:40:49 +01:00
Leendert de Borst
450ca6a6f4 Update db-import command (#506) 2024-12-31 21:34:23 +01:00
Leendert de Borst
4c31912d73 Add db import and export commands to install.sh (#506) 2024-12-31 21:18:44 +01:00
dependabot[bot]
367be5a409 Bump NUnit from 4.3.1 to 4.3.2
Bumps [NUnit](https://github.com/nunit/nunit) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/nunit/nunit/releases)
- [Changelog](https://github.com/nunit/nunit/blob/main/CHANGES.md)
- [Commits](https://github.com/nunit/nunit/compare/4.3.1...4.3.2)

---
updated-dependencies:
- dependency-name: NUnit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-31 17:57:22 +01:00
dependabot[bot]
b6cf46ab91 Bump HtmlAgilityPack from 1.11.71 to 1.11.72
Bumps [HtmlAgilityPack](https://github.com/zzzprojects/html-agility-pack) from 1.11.71 to 1.11.72.
- [Release notes](https://github.com/zzzprojects/html-agility-pack/releases)
- [Commits](https://github.com/zzzprojects/html-agility-pack/compare/v1.11.71...v1.11.72)

---
updated-dependencies:
- dependency-name: HtmlAgilityPack
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-31 17:57:14 +01:00
Leendert de Borst
0da0bd0b17 Merge pull request #505 from lanedirt/503-searching-in-admin-table-pages-defocuses-search-field-after-every-character-typed
Do not show loading indicator when refreshing in admin
2024-12-31 17:56:01 +01:00
Leendert de Borst
d8ccaad806 Merge pull request #504 from lanedirt/493-fix-dataprotection-api-errors-when-running-a-clean-docker-install
Do not log dataprotection warnings to database log
2024-12-31 17:50:07 +01:00
Leendert de Borst
656210e4f6 Do not show loading indicator when refreshing in admin (#503) 2024-12-31 17:48:29 +01:00
Leendert de Borst
c3c85bc10e Do not log dataprotection warnings to database log but keep for filelog (#493) 2024-12-31 17:37:17 +01:00
Leendert de Borst
693ad0b581 Update CONTRIBUTING.md 2024-12-31 15:29:32 +01:00
Leendert de Borst
94ad51059e Merge pull request #500 from lanedirt/491-link-from-contributindmd-to-the-official-docs-page
Update contributing documentation
2024-12-30 11:51:12 +01:00
Leendert de Borst
9374780a5b Update CONTRIBUTING.md (#491) 2024-12-29 12:30:34 +01:00
Leendert de Borst
3263360be5 Merge pull request #492 from lanedirt/190-add-postgresql-as-database-option-to-prevent-concurrency-locking-due-to-sqlite-limitations
Switch SQLite to PostgreSQL database engine
2024-12-29 12:15:32 +01:00
Leendert de Borst
1151089d59 Fix merge conflict (#190) 2024-12-28 17:06:50 +01:00
Leendert de Borst
d39ecf69e8 Merge pull request #499 from lanedirt/498-make-user-refreshtoken-lifetime-configurable-via-server-settings
Make UserRefreshToken lifetime configurable via admin
2024-12-28 16:58:29 +01:00
Leendert de Borst
9caea03460 Update AuthTests.cs (#498) 2024-12-28 16:45:06 +01:00
Leendert de Borst
32879e09a8 Make UserRefreshToken lifetime configurable via admin (#498) 2024-12-28 16:35:21 +01:00
Leendert de Borst
d3518eca6c Update install.sh add install.sh version check to install command itself (#190) 2024-12-28 15:52:33 +01:00
Leendert de Borst
329ae185ad Update docs (#190) 2024-12-28 14:28:09 +01:00
dependabot[bot]
888054e8ed Bump NUnit.Analyzers from 4.4.0 to 4.5.0
Bumps [NUnit.Analyzers](https://github.com/nunit/nunit.analyzers) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/nunit/nunit.analyzers/releases)
- [Changelog](https://github.com/nunit/nunit.analyzers/blob/master/CHANGES.md)
- [Commits](https://github.com/nunit/nunit.analyzers/compare/4.4.0...4.5.0)

---
updated-dependencies:
- dependency-name: NUnit.Analyzers
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-27 17:46:07 +01:00
dependabot[bot]
0d141e2c7c Bump NUnit from 4.3.0 to 4.3.1
Bumps [NUnit](https://github.com/nunit/nunit) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/nunit/nunit/releases)
- [Changelog](https://github.com/nunit/nunit/blob/main/CHANGES.md)
- [Commits](https://github.com/nunit/nunit/compare/4.3.0...4.3.1)

---
updated-dependencies:
- dependency-name: NUnit
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-27 17:29:08 +01:00
Leendert de Borst
33b930b58a Merge pull request #497 from lanedirt/495-improve-2fa-login-flow-to-automatically-submit-when-6-digits-have-been-entered
Auto submit login form when 2fa code is entered
2024-12-27 17:26:39 +01:00
Leendert de Borst
ad9eb79e9e Update CodeLockoutTests.cs (#495) 2024-12-27 16:56:20 +01:00
Leendert de Borst
cd46578576 Auto submit login form when 2fa code is entered (#495) 2024-12-27 10:43:04 +01:00
Leendert de Borst
e577d6fee4 Print warnring but do not exit on image pull fail (#190) 2024-12-26 01:30:32 +01:00
Leendert de Borst
9d1923d3ea Refactor dev db start (#190) 2024-12-26 01:26:43 +01:00
Leendert de Borst
d77c28184c Refactor (#190) 2024-12-26 01:13:40 +01:00
Leendert de Borst
180de219c8 Update installer to look at latest release instead of main (#190) 2024-12-26 00:41:58 +01:00
Leendert de Borst
17e4f614d8 Refactor postgres docker setup and update docs (#190) 2024-12-26 00:39:19 +01:00
Leendert de Borst
747e0910cb Update SQLite ef model (#190) 2024-12-25 23:10:24 +01:00
Leendert de Borst
fc85f34218 Update useremailclaims setnull to be compatible with PostgreSQL (#190) 2024-12-25 23:01:54 +01:00
Leendert de Borst
f0e0e9c03e Add TaskRunnerJobs to migration (#190) 2024-12-25 22:55:10 +01:00
Leendert de Borst
2631a1f0b1 Update migration (#190) 2024-12-25 22:47:44 +01:00
Leendert de Borst
254104e12d Update migration logging (#190) 2024-12-25 22:46:08 +01:00
Leendert de Borst
a75d5c7a34 Update migration add data truncation if source data exceeds length (#190) 2024-12-25 22:41:56 +01:00
Leendert de Borst
bf40539e92 Update dockerignore to ignore data directories during build (#190) 2024-12-25 22:41:40 +01:00
Leendert de Borst
cb330219ab Refactor postgres db folder creation (#190) 2024-12-25 22:26:58 +01:00
Leendert de Borst
4f5e822722 Include postgres empty dir in git to ensure correct permissions (#190) 2024-12-25 22:23:43 +01:00
Leendert de Borst
96997c7d8d Update docs (#190) 2024-12-25 22:21:19 +01:00
Leendert de Borst
f959b7dc91 Update install.sh (#190) 2024-12-25 22:21:13 +01:00
Leendert de Borst
59599f43a3 Update docs (#190) 2024-12-25 16:53:30 +01:00
Leendert de Borst
b5e575051c Update migration logic to reset auto increment id (#190) 2024-12-25 16:45:17 +01:00
Leendert de Borst
433664d85d Fix admin redirect absolute URL bug (#190) 2024-12-25 14:40:44 +01:00
Leendert de Borst
82b2b75127 Update docs with new update instructions (#190) 2024-12-25 14:24:31 +01:00
Leendert de Borst
577e02d761 Update install script (#190) 2024-12-25 14:24:16 +01:00
Leendert de Borst
26b1c4e044 Load security page components async (#190) 2024-12-25 12:11:17 +01:00
Leendert de Borst
3872678039 Sanitize email when retrieving emails for emailbox (#190) 2024-12-24 22:20:44 +01:00
Leendert de Borst
80cc72eb22 Fix RecentEmails.razor dispose bug (#190) 2024-12-24 22:20:12 +01:00
Leendert de Borst
141a291ace Fix bug in db sync tests (#190) 2024-12-24 20:49:38 +01:00
Leendert de Borst
3b5e944417 Refactor (#190) 2024-12-24 15:48:34 +01:00
Leendert de Borst
65553e0918 Update postgresql date column types to improve compatiblity (#190) 2024-12-24 15:27:04 +01:00
Leendert de Borst
a7502d42e4 Fix migration tool params called from install.sh (#190) 2024-12-24 14:56:37 +01:00
Leendert de Borst
4d43acb53f Add build container start/stop/restart commands (#190) 2024-12-24 13:50:49 +01:00
Leendert de Borst
14ac94b78a Add migrate-db command to install.sh (#190) 2024-12-24 13:08:48 +01:00
Leendert de Borst
361f4b8817 Added migration logic from sqlite to postgresql (#190) 2024-12-24 12:53:22 +01:00
Leendert de Borst
7a62ddcf6a Enable postgresql legacy timestamp behavior (#190) 2024-12-24 12:39:57 +01:00
Leendert de Borst
6b59200df2 Fix migrations (#190) 2024-12-24 11:59:37 +01:00
Leendert de Borst
6a0699318c Add sqlite migrations to be in sync with postgresql model (#190) 2024-12-23 21:41:25 +01:00
Leendert de Borst
f6e2648a53 Update GitHub actions for postgresql (#190) 2024-12-23 20:52:39 +01:00
Leendert de Borst
4b8e4c907e Refactor WebApplicationFactoryFixture (#190) 2024-12-23 20:10:47 +01:00
Leendert de Borst
30804cc973 Update DataProtectionExtensions.cs (#190) 2024-12-23 18:00:47 +01:00
Leendert de Borst
8edfc3d0d6 Update Logout.razor (#190) 2024-12-23 17:11:30 +01:00
Leendert de Borst
4fb5087c82 Add local postgresql dev database (#190) 2024-12-23 16:31:02 +01:00
Leendert de Borst
cf454d2bb8 Add postgres healthcheck to docker-compose.yml (#190) 2024-12-23 16:03:59 +01:00
Leendert de Borst
0a577873ee Update install.sh to create postgres credentials (#190) 2024-12-23 15:40:00 +01:00
Leendert de Borst
32c8e48d45 Update Dockerfile (#190) 2024-12-23 15:25:18 +01:00
Leendert de Borst
564ae54de8 Update postgresql factory to support env vars if available (#190) 2024-12-23 15:11:34 +01:00
Leendert de Borst
d9f4f8d121 Update Dockerfile (#190) 2024-12-23 14:38:07 +01:00
Leendert de Borst
728b20b489 Update install.sh (#190) 2024-12-23 14:18:47 +01:00
Leendert de Borst
219f0bc9cc Update db configuration (#190) 2024-12-23 14:17:17 +01:00
Leendert de Borst
9735df0436 Update install.sh to generate postgresql credentials (#190) 2024-12-23 13:57:01 +01:00
Leendert de Borst
78a872a67d Refactor smtpserver and taskrunner so all tests pass (#190) 2024-12-23 12:58:07 +01:00
Leendert de Borst
77a48ea4e9 Refactor admin so all tests pass (#190) 2024-12-23 12:16:05 +01:00
Leendert de Borst
22538ae000 Refactor datetime to always use UTC (#190) 2024-12-22 21:36:01 +01:00
Leendert de Borst
db632c3edb Refactor SmtpService to use new dbcontextfactory (#190) 2024-12-22 20:05:05 +01:00
Leendert de Borst
1c53addcaa Refactor WebApi to use new dbcontextfactory (#190) 2024-12-22 18:58:24 +01:00
Leendert de Borst
817404cd08 Refactor UserService delete unused methods causing concurrency issues (#190) 2024-12-22 14:10:30 +01:00
Leendert de Borst
9062cdc701 Refactor admin project to use dbcontextfactory (#190) 2024-12-22 11:53:22 +01:00
Leendert de Borst
e45866fa67 Update EF models to not use driver specific fields (#190) 2024-12-22 11:26:16 +01:00
Leendert de Borst
8fbd10caaa Update admin project to use new IAliasServerDbContextFactory (#190) 2024-12-22 00:37:13 +01:00
Leendert de Borst
54d54f28b4 Move migrations for db engines to their respective folders (#190) 2024-12-22 00:07:31 +01:00
Leendert de Borst
3116aa5a1f Update postgresql.conf (#190) 2024-12-21 12:17:17 +01:00
Leendert de Borst
eb45358532 Update gitignore for db files (#190) 2024-12-21 12:09:43 +01:00
Leendert de Borst
03fd047cb4 Add postgresql docker container scaffolding (#190) 2024-12-21 11:56:08 +01:00
Leendert de Borst
6a7fc9c5ba Merge pull request #488 from lanedirt/343-add-option-to-disable-new-user-registration-via-installsh
Update install.sh
2024-12-20 20:23:06 +01:00
Leendert de Borst
62700de9ad Update install.sh (#343) 2024-12-20 20:22:43 +01:00
Leendert de Borst
edeaa77299 Merge pull request #487 from lanedirt/342-add-option-to-block-existing-user-in-admin
Add option to block existing user in admin
2024-12-20 19:57:39 +01:00
Leendert de Borst
84b93924f5 Add user block checks to api and add tests (#342) 2024-12-20 19:47:12 +01:00
Leendert de Borst
400e702753 Add user blocked status toggle to admin (#342) 2024-12-20 18:53:28 +01:00
Leendert de Borst
147f8db5d1 Add Blocked column to users table (#342) 2024-12-20 18:53:01 +01:00
Leendert de Borst
8e1470bc1b Merge pull request #486 from lanedirt/343-add-option-to-disable-new-user-registration-via-installsh
Add option to disable new user registration via install.sh
2024-12-20 18:24:50 +01:00
Leendert de Borst
50853bf011 Update tests (#343) 2024-12-20 17:30:28 +01:00
Leendert de Borst
07dd90a705 Add public registration enabled check to client (#343) 2024-12-20 16:52:05 +01:00
Leendert de Borst
a7a7d6d82b Rename public registration enabled flag (#343) 2024-12-20 16:51:51 +01:00
Leendert de Borst
249efe54b0 Add public registration env flag check to api (#343) 2024-12-20 16:06:15 +01:00
Leendert de Borst
20eb3e5ff4 Add public registration setting to install.sh (#343) 2024-12-20 15:56:30 +01:00
Leendert de Borst
ba15c446d9 Merge pull request #484 from lanedirt/483-dark-mode-text-readability-tweaks
Dark mode text readability tweaks
2024-12-19 15:54:55 +01:00
Leendert de Borst
5ea9f4ee08 Merge pull request #482 from lanedirt/481-add-username-sanity-check-on-vault-save-to-prevent-db-corruption-between-user-sessions
Add username sanity check to vault save to prevent potential vault collision/corruption
2024-12-19 15:54:47 +01:00
Leendert de Borst
1e7b7b172a Update tests to work with changed notifications (#481) 2024-12-19 15:39:22 +01:00
Leendert de Borst
35f6565c8b Update DbService.cs to fix merge return status (#481) 2024-12-19 15:16:28 +01:00
Leendert de Borst
8cb99c997d Update Logo.razor (#483) 2024-12-19 15:03:48 +01:00
Leendert de Borst
16cf528b7f Add dark mode tweaks to admin error page (#483) 2024-12-19 14:55:25 +01:00
Leendert de Borst
8ad3eb9bd5 Add dark mode tweaks to client two factor code page (#483) 2024-12-19 14:55:14 +01:00
Leendert de Borst
2d59c40e24 Refactor username sanity check to associate with vault load instead of login (#481) 2024-12-19 14:39:05 +01:00
Leendert de Borst
33283e07be Set username on unlock (#481) 2024-12-19 13:34:14 +01:00
Leendert de Borst
fbf5880370 Add username sanity check to vault save (#481) 2024-12-19 12:14:06 +01:00
Leendert de Borst
be4a74ef3c Update README.md 2024-12-18 10:54:09 +01:00
Leendert de Borst
a759091755 Update AppInfo.cs (#479) 2024-12-16 16:55:58 +01:00
dependabot[bot]
8dc99c09a8 Bump Swashbuckle.AspNetCore from 7.1.0 to 7.2.0
Bumps [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) from 7.1.0 to 7.2.0.
- [Release notes](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/releases)
- [Commits](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/compare/v7.1.0...v7.2.0)

---
updated-dependencies:
- dependency-name: Swashbuckle.AspNetCore
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 12:19:17 +01:00
dependabot[bot]
b9ec4baf66 Bump NUglify from 1.21.10 to 1.21.11
Bumps [NUglify](https://github.com/trullock/NUglify) from 1.21.10 to 1.21.11.
- [Release notes](https://github.com/trullock/NUglify/releases)
- [Changelog](https://github.com/trullock/NUglify/blob/master/changelog.md)
- [Commits](https://github.com/trullock/NUglify/compare/v1.21.10...v1.21.11)

---
updated-dependencies:
- dependency-name: NUglify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 12:19:10 +01:00
Leendert de Borst
71ed62cdcb Merge pull request #478 from lanedirt/469-webassembly-required-error-not-visible-in-client-app
Add E2E test for browser with WASM disabled
2024-12-16 12:19:00 +01:00
dependabot[bot]
2bbad8c75c Bump NUnit from 4.2.2 to 4.3.0
Bumps [NUnit](https://github.com/nunit/nunit) from 4.2.2 to 4.3.0.
- [Release notes](https://github.com/nunit/nunit/releases)
- [Changelog](https://github.com/nunit/nunit/blob/main/CHANGES.md)
- [Commits](https://github.com/nunit/nunit/compare/4.2.2...4.3.0)

---
updated-dependencies:
- dependency-name: NUnit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:59 +01:00
dependabot[bot]
f02b841eea Bump Serilog and Serilog.Settings.Configuration
Bumps [Serilog](https://github.com/serilog/serilog) and [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration). These dependencies needed to be updated together.

Updates `Serilog` from 4.2.0 to 4.2.0
- [Release notes](https://github.com/serilog/serilog/releases)
- [Commits](https://github.com/serilog/serilog/compare/v4.2.0...v4.2.0)

Updates `Serilog.Settings.Configuration` from 8.0.4 to 9.0.0
- [Release notes](https://github.com/serilog/serilog-settings-configuration/releases)
- [Changelog](https://github.com/serilog/serilog-settings-configuration/blob/dev/CHANGES.md)
- [Commits](https://github.com/serilog/serilog-settings-configuration/compare/v8.0.4...v9.0.0)

---
updated-dependencies:
- dependency-name: Serilog
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: Serilog.Settings.Configuration
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:36 +01:00
dependabot[bot]
f6fc5af8ac Bump MailKit from 4.8.0 to 4.9.0
Bumps [MailKit](https://github.com/jstedfast/MailKit) from 4.8.0 to 4.9.0.
- [Changelog](https://github.com/jstedfast/MailKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MailKit/compare/4.8.0...4.9.0)

---
updated-dependencies:
- dependency-name: MailKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:30 +01:00
dependabot[bot]
1d1155bf0e Bump MimeKit from 4.8.0 to 4.9.0
Bumps [MimeKit](https://github.com/jstedfast/MimeKit) from 4.8.0 to 4.9.0.
- [Changelog](https://github.com/jstedfast/MimeKit/blob/master/ReleaseNotes.md)
- [Commits](https://github.com/jstedfast/MimeKit/compare/4.8.0...4.9.0)

---
updated-dependencies:
- dependency-name: MimeKit
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-16 11:24:24 +01:00
Leendert de Borst
2632211af6 Merge pull request #470 from lanedirt/469-webassembly-required-error-not-visible-in-client-app
Show error if client does not support WebAssembly
2024-12-16 10:34:20 +01:00
Leendert de Borst
05cca6998e Merge pull request #468 from lanedirt/467-task-runner-jobs-do-not-always-run-at-configured-time
Add task runner job table for tracking task runner historic runs
2024-12-16 10:18:13 +01:00
Leendert de Borst
c4a8a20a62 Add E2E test for browser with WASM disabled (#469) 2024-12-15 17:05:31 +01:00
Leendert de Borst
f2c6af9ccb Update install.sh URL comment (#469) 2024-12-15 16:43:48 +01:00
Leendert de Borst
e94201acda Tweak logo on mobile view auth area (#469) 2024-12-15 16:28:57 +01:00
Leendert de Borst
9e03473208 Show error message when client does not support WebAssembly (#469) 2024-12-15 16:28:41 +01:00
Leendert de Borst
0c5b2fb1da Add task runner job table and manual start button (#467) 2024-12-15 15:59:51 +01:00
Leendert de Borst
a5c4a7618d Update AliasServerDbContext.cs so pragma settings are applied correctly (#467) 2024-12-15 14:53:33 +01:00
224 changed files with 9984 additions and 1370 deletions

View File

@@ -22,4 +22,17 @@
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
README.md
# Exclude AliasVault data directories
database/
logs/
certificates/
# Exclude git directory
.git/
# Exclude development files
*.log
*.env
*.env.*

View File

@@ -6,3 +6,5 @@ ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
PRIVATE_EMAIL_DOMAINS=
SMTP_TLS_ENABLED=false
LETSENCRYPT_ENABLED=false
POSTGRES_PASSWORD=
SUPPORT_EMAIL=

View File

@@ -25,6 +25,9 @@ jobs:
- name: Build
run: dotnet build
- name: Start dev database
run: ./install.sh configure-dev-db start
- name: Ensure browsers are installed
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps

View File

@@ -29,6 +29,9 @@ jobs:
- name: Build
run: dotnet build
- name: Start dev database
run: ./install.sh configure-dev-db start
- name: Ensure browsers are installed
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps

View File

@@ -25,6 +25,9 @@ jobs:
- name: Build
run: dotnet build
- name: Start dev database
run: ./install.sh configure-dev-db start
- name: Ensure browsers are installed
run: pwsh src/Tests/AliasVault.E2ETests/bin/Debug/net9.0/playwright.ps1 install --with-deps

View File

@@ -25,5 +25,8 @@ jobs:
- name: Build
run: dotnet build
- name: Start dev database
run: ./install.sh configure-dev-db start
- name: Run integration tests
run: dotnet test src/Tests/AliasVault.IntegrationTests --no-build --verbosity normal

View File

@@ -38,6 +38,14 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}
- name: Build and push Postgres image
uses: docker/build-push-action@v5
with:
context: .
file: src/Databases/AliasServerDb/Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-postgres:${{ github.ref_name }}
- name: Build and push API image
uses: docker/build-push-action@v5
with:
@@ -62,6 +70,14 @@ jobs:
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-admin:${{ github.ref_name }}
- name: Build and push Reverse Proxy image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
- name: Build and push SMTP image
uses: docker/build-push-action@v5
with:
@@ -78,14 +94,6 @@ jobs:
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-task-runner:${{ github.ref_name }}
- name: Build and push Reverse Proxy image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: true
tags: ${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:latest,${{ env.REGISTRY }}/${{ env.REPO_LOWER }}-reverse-proxy:${{ github.ref_name }}
- name: Build and push InstallCli image
uses: docker/build-push-action@v5
with:

8
.gitignore vendored
View File

@@ -272,6 +272,10 @@ ServiceFabricBackup/
*.sqlite-shm
*.sqlite-wal
# SQL files
*.sql
*.sql.gz
# Business Intelligence projects
*.rdl.data
*.bim.layout
@@ -407,3 +411,7 @@ certificates/letsencrypt/**
docs/_site
docs/vendor
docs/.bundle
# Database files
database/postgres
database/postgres-dev

View File

@@ -1,101 +1,14 @@
# Contributing
This document is a work-in-progress and will be expanded as time goes on. If you have any questions feel free to open a issue on GitHub.
# Contributing to the source code
We welcome contributions to AliasVault. Please read the guidelines on the official AliasVault docs website on how to get your local development environment setup and the general contribution guidelines:
Note: all instructions below are based on MacOS. If you are using a different operating system, you may need to adjust the commands accordingly.
https://docs.aliasvault.net/misc/dev/contributing.html
## Getting Started
In order to contribute to this project follow these instructions to setup your local environment:
> Tip: if the URL above is not available, the raw doc pages can also be found in the `docs` folder in this repository.
### 1. Clone the repository
## Contributing to the documentation
The docs are built using Jekyll and automatically deploy to GitHub Pages via GitHub Actions. You can build the docs locally by running `docker compose up` in in the `./docs` folder.
```bash
git clone https://github.com/lanedirt/AliasVault.git
cd AliasVault
```
### 2. Copy pre-commit hook script to .git/hooks directory
**Important**: All commits in this repo are required to contain a reference to a GitHub issue in the format of "your commit message (#123)" where "123" references the GitHub issue number.
The pre-commit hook script below will check the commit message before allowing the commit to proceed. If the commit message is invalid, the commit will be aborted.
```bash
# Copy the commit-msg hook script to the .git/hooks directory
cp .github/hooks/commit-msg .git/hooks/commit-msg
# Make the script executable
chmod +x .git/hooks/commit-msg
```
### 3. Install the latest version of .NET SDK 8
```bash
# Install .NET SDK 8
# On MacOS via brew:
brew install --cask dotnet-sdk
# On Windows via winget
winget install Microsoft.DotNet.SDK.8
```
### 4. Install dotnet CLI EF Tools
```bash
# Install dotnet EF tools globally
dotnet tool install --global dotnet-ef
# Include dotnet tools in your PATH
nano ~/.zshrc
# Add the following line to your .zshrc file
export PATH="$PATH:$HOME/.dotnet/tools"
# Start a new terminal and test that this command works:
dotnet ef
```
### 5. Run Tailwind CSS compiler while changing HTML files to update compiled CSS
```bash
npm run build:css
```
### 6. Install Playwright in order to locally run NUnit E2E (end-to-end) tests
```bash
# First install PowerShell for Mac (if you don't have it already)
brew install powershell/tap/powershell
# Install Playwright
dotnet tool install --global Microsoft.Playwright.CLI
# Run Playwright install script to download local browsers
# 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.Client appsettings.Development.json
The WASM client app supports a development specific appsettings.json file. This appsettings file is optional but can override various options to make debugging easier.
The docs site is based on the open-source template called Just The Docs. Find more information about how this template works in the [official docs](https://just-the-docs.github.io/just-the-docs/).
1. Copy `wwwroot/appsettings.json` to `wwwroot/appsettings.Development.json`
Here is an example file with the various options explained:
```
{
"ApiUrl": "http://localhost:5092",
"PrivateEmailDomains": ["example.tld"],
"SupportEmail": "support@example.tld",
"UseDebugEncryptionKey": "true",
"CryptographyOverrideType" : "Argon2Id",
"CryptographyOverrideSettings" : "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
}
```
- 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.
- CryptographyOverrideType
- This setting allows overriding the default encryption type (Argon2id) with a different encryption type. This is useful for testing different encryption types without having to change code.
- CryptographyOverrideSettings
- This setting allows overriding the default encryption settings (Argon2id) with different settings. This is useful for testing different encryption settings without having to change code. The default Argon2id settings
are defined in the project as `Utilities/Cryptography/Cryptography.Client/Defaults.cs`. These default settings
are focused on security but NOT performance. Normally for key derivation purposes the slower/heavier the algorithm
the better protection against attackers. For production builds this is what we want, however in case of automated testing or debugging extra performance can be gained by tweaking (lowering) these settings.
To make changes to the AliasVault documentation please make a PR that directly edits the `docs` markdown files in this repository.

View File

@@ -1,5 +1,7 @@
<div align="center">
🌟 **If you find this project useful, please consider giving it a star!** 🌟
<h1><img src="https://github.com/user-attachments/assets/933c8b45-a190-4df6-913e-b7c64ad9938b" width="40" /> AliasVault</h1>
<p align="center">
@@ -50,7 +52,7 @@ This method uses pre-built Docker images and works on minimal hardware specifica
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
- 1 vCPU
- 512MB RAM
- 1GB RAM
- 16GB disk space
- Docker installed

View File

@@ -1,9 +1,9 @@
services:
reverse-proxy:
image: aliasvault-reverse-proxy
postgres:
image: aliasvault-postgres
build:
context: .
dockerfile: Dockerfile
dockerfile: src/Databases/AliasServerDb/Dockerfile
client:
image: aliasvault-client
@@ -23,6 +23,12 @@ services:
context: .
dockerfile: src/AliasVault.Admin/Dockerfile
reverse-proxy:
image: aliasvault-reverse-proxy
build:
context: .
dockerfile: Dockerfile
smtp:
image: aliasvault-smtp
build:

20
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,20 @@
services:
postgres-dev:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- ./database/postgres-dev:/var/lib/postgresql/data:rw
- ./src/Databases/AliasServerDb/postgresql.conf:/etc/postgresql/postgresql.conf
environment:
- POSTGRES_DB=aliasvault
- POSTGRES_USER=aliasvault
- POSTGRES_PASSWORD=password
restart: "no"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aliasvault"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
command: ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]

View File

@@ -1,21 +1,17 @@
services:
reverse-proxy:
image: ghcr.io/lanedirt/aliasvault-reverse-proxy:latest
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
postgres:
image: ghcr.io/lanedirt/aliasvault-postgres:latest
volumes:
- ./certificates/ssl:/etc/nginx/ssl:rw
- ./certificates/letsencrypt:/etc/nginx/ssl-letsencrypt:rw
- ./certificates/letsencrypt/www:/var/www/certbot:rw
depends_on:
- admin
- client
- api
- smtp
restart: always
- ./database/postgres:/var/lib/postgresql/data:rw
env_file:
- .env
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U aliasvault"]
interval: 5s
timeout: 5s
retries: 5
start_period: 10s
client:
image: ghcr.io/lanedirt/aliasvault-client:latest
@@ -38,6 +34,11 @@ services:
restart: always
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
environment:
ConnectionStrings__AliasServerDbContext: "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}"
admin:
image: ghcr.io/lanedirt/aliasvault-admin:latest
@@ -50,6 +51,29 @@ services:
restart: always
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
environment:
ConnectionStrings__AliasServerDbContext: "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}"
reverse-proxy:
image: ghcr.io/lanedirt/aliasvault-reverse-proxy:latest
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
volumes:
- ./certificates/ssl:/etc/nginx/ssl:rw
- ./certificates/letsencrypt:/etc/nginx/ssl-letsencrypt:rw
- ./certificates/letsencrypt/www:/var/www/certbot:rw
depends_on:
- admin
- client
- api
- smtp
restart: always
env_file:
- .env
smtp:
image: ghcr.io/lanedirt/aliasvault-smtp:latest
@@ -62,6 +86,11 @@ services:
restart: always
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
environment:
ConnectionStrings__AliasServerDbContext: "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}"
task-runner:
image: ghcr.io/lanedirt/aliasvault-task-runner:latest
@@ -71,3 +100,8 @@ services:
restart: always
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
environment:
ConnectionStrings__AliasServerDbContext: "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}"

View File

@@ -9,7 +9,7 @@ nav_order: 1
Instead of using the pre-built Docker images, you can also build the images from source yourself. This allows you to build a specific version of AliasVault and/or to make changes to the source code.
Building from source requires more resources:
- Minimum 2GB RAM (more RAM will speed up build time)
- Minimum 4GB RAM (more RAM will speed up build time)
- At least 1 vCPU
- 40GB+ disk space (for dependencies and build artifacts)
- Docker installed

View File

@@ -0,0 +1,22 @@
---
layout: default
title: Database Backup
parent: Advanced
nav_order: 2
---
# Database Backup
In order to backup the database, you can use the `install.sh` script. This script will stop all services, export the database to a file, and then restart the services.
```bash
./install.sh db-backup > backup.sql.gz
```
# Database Restore
To restore the database, you can use the `install.sh` script. This script will stop all services, drop the database, import the database from a file, and then restart the services.
```bash
./install.sh db-restore < backup.sql.gz
```

View File

@@ -2,7 +2,7 @@
layout: default
title: Manual Setup
parent: Advanced
nav_order: 2
nav_order: 3
---
# Manual Setup
@@ -20,7 +20,7 @@ If you prefer to manually set up AliasVault, this README provides step-by-step i
Create the following directories in your project root:
```bash
mkdir -p certificates/ssl certificates/app database logs/msbuild
mkdir -p certificates/ssl certificates/app database/postgres
```
2. **Create .env file**
@@ -61,7 +61,21 @@ If you prefer to manually set up AliasVault, this README provides step-by-step i
DATA_PROTECTION_CERT_PASS=your_generated_password_here
```
6. **Set PRIVATE_EMAIL_DOMAINS**
6. **Configure PostgreSQL Settings**
Set the following PostgreSQL-related variables in your .env file:
```bash
# Database name (default: aliasvault)
POSTGRES_DB=aliasvault
# Database user (default: aliasvault)
POSTGRES_USER=aliasvault
# Generate a secure password for PostgreSQL
POSTGRES_PASSWORD=$(openssl rand -base64 32)
```
7. **Set PRIVATE_EMAIL_DOMAINS**
Update the .env file with allowed email domains. Use DISABLED.TLD to disable email support:
```bash
@@ -72,14 +86,14 @@ If you prefer to manually set up AliasVault, this README provides step-by-step i
PRIVATE_EMAIL_DOMAINS=DISABLED.TLD
```
7. **Set SUPPORT_EMAIL (Optional)**
8. **Set SUPPORT_EMAIL (Optional)**
Add a support email address if desired:
```bash
SUPPORT_EMAIL=support@yourdomain.com
```
8. **Generate admin password**
9. **Generate admin password**
Build the Docker image for password hashing:
```bash
@@ -97,7 +111,7 @@ If you prefer to manually set up AliasVault, this README provides step-by-step i
ADMIN_PASSWORD_GENERATED=2024-01-01T00:00:00Z
```
9. **Build and start Docker containers**
10. **Build and start Docker containers**
Build the Docker Compose stack:
```bash
@@ -109,22 +123,23 @@ If you prefer to manually set up AliasVault, this README provides step-by-step i
docker compose up -d
```
10. **Access AliasVault**
11. **Access AliasVault**
AliasVault should now be running. You can access it at:
- Admin Panel: https://localhost/admin
- Username: admin
- Password: [Use the password you set in step 8]
- Password: [Use the password you set in step 9]
- Client Website: https://localhost/
- Create your own account from here
## Important Notes
- Make sure to save the admin password you used in step 8 in a secure location.
- If you need to reset the admin password in the future, repeat step 8 and restart the Docker containers.
- Make sure to save both the admin password and PostgreSQL password in a secure location.
- If you need to reset the admin password in the future, repeat step 9 and restart the Docker containers.
- Always keep your .env file secure and do not share it, as it contains sensitive information.
- The PostgreSQL data is persisted in the `database/postgres` directory.
## Troubleshooting
@@ -134,7 +149,11 @@ If you encounter any issues during the setup:
```bash
docker compose logs
```
2. Ensure all required ports (80 and 443) are available and not being used by other services.
2. Ensure all required ports (80, 443, and 5432) are available and not being used by other services.
3. Verify that all environment variables in the .env file are set correctly.
4. Check PostgreSQL container logs specifically:
```bash
docker compose logs postgres
```
For further assistance, please refer to the project documentation or seek support through the appropriate channels.

View File

@@ -20,7 +20,7 @@ To get AliasVault up and running quickly, run the install script to pull pre-bui
### Hardware requirements
- Linux VM with root access (Ubuntu or RHEL based distros recommended)
- 1 vCPU
- 512MB RAM
- 1GB RAM
- 16GB disk space
- Docker installed
@@ -128,7 +128,21 @@ If you encounter any issues, feel free to open an issue on the [GitHub repositor
---
## 4. Troubleshooting
## 4. Configure Account Registration
By default, AliasVault is configured to allow public registration of new accounts. This means that anyone can create a new account on your server.
If you want to disable public registration, you can do so by running the install script with the `configure-registration` option and then choosing option 2.
```bash
./install.sh configure-registration
```
> Note: disabling public registration means the ability to create new accounts in the AliasVault client is disabled for everyone, including administrators. Accounts cannot be created outside of the client because of the end-to-end encryption employed by AliasVault. So make sure you have created your own account(s) before disabling public registration.
---
## 5. Troubleshooting
### Resetting the admin password
If you have lost your admin password, you can reset it by running the install script with the `reset-password` option. This will generate a new random password and update the .env file with it. After that it will restart the AliasVault containers to apply the changes.

View File

@@ -6,16 +6,26 @@ nav_order: 3
---
# Updating AliasVault
To update AliasVault to the latest version, run the install script with the `update` option. This will pull the latest version of AliasVault from GitHub and restart all containers.
{: .no_toc }
<details open markdown="block">
<summary>
Table of contents
</summary>
{: .text-delta }
1. TOC
{:toc}
</details>
## Before You Begin
You can see the latest available version of AliasVault on [GitHub](https://github.com/lanedirt/AliasVault/releases).
{: .warning }
Before updating, it's recommended to backup your database and other important data. You can do this by making
a copy of the `database` and `certificates` directories.
## Updating to the latest available version
To update to the latest version, run the install script with the `update` option. The script will check for the latest version and prompt you to confirm the update. Follow the prompts to complete the update.
## Standard Update Process
For most version updates, you can use the standard update process:
```bash
./install.sh update
@@ -23,7 +33,14 @@ To update to the latest version, run the install script with the `update` option
> Tip: to skip the confirmation prompts and automatically proceed with the update, use the `-y` flag: `./install.sh update -y`
## Updating the installer script
## Version-Specific Upgrade Guides
Some versions require additional steps during upgrade. If you are upgrading from an older version, please check the relevant upgrade guide below:
- [Updating to v0.10.0](v0-10-0.html) - SQLite to PostgreSQL migration
## Additional Update Options
### Updating the installer script
The installer script can check for and apply updates to itself. This is done as part of the `update` command. However you can also update the installer script separately with the `update-installer` command. This is useful if you want to update the installer script without updating AliasVault itself, e.g. as a separate step during CI/CD pipeline.
```bash
@@ -32,8 +49,8 @@ The installer script can check for and apply updates to itself. This is done as
> Tip: to skip the confirmation prompts and automatically proceed with the update, use the `-y` flag: `./install.sh update-installer -y`
## Installing a specific version
To install a specific version and skip the automatic version checks, run the install script with the `install` option and specify the version you want to install.
### Installing a specific version
To install a specific version and skip the automatic version checks, run the install script with the `install` option and specify the version you want to install. Note that downgrading is not supported officially and may lead to unexpected issues.
```bash
./install.sh install <version>

View File

@@ -0,0 +1,39 @@
---
layout: default
title: Update to v0.10.0
parent: Update
grand_parent: Installation Guide
nav_order: 1
---
# Upgrading to v0.10.0
{: .no_toc }
This guide covers the upgrade process from version < v0.10.0 to v0.10.0 or newer, which includes a one-time database migration from SQLite to PostgreSQL.
The v0.10.0 release introduces a new database backend, PostgreSQL, which replaces SQLite. This change is required because SQLite is not suitable for environments with concurrent writes that AliasVault requires.
A built-in database migration tool is included in the installer script to help you migrate your data from SQLite to PostgreSQL.
## Update Steps
1. First, backup your existing SQLite database:
```bash
cp database/AliasServerDb.sqlite database/AliasServerDb.sqlite.backup
```
2. Update AliasVault to the latest version:
```bash
./install.sh update
```
3. Run the database migration tool:
```bash
./install.sh migrate-db
```
4. After the migration has completed successfully, restart all AliasVault containers:
```bash
./install.sh restart
```
5. Test the upgrade by logging in to the admin panel and checking that your data is intact.
If you encounter any issues during the upgrade, please create an issue on the [GitHub repository](https://github.com/lanedirt/AliasVault/issues) or contact via Discord.

View File

@@ -1,27 +0,0 @@
---
layout: default
title: Configure SQLite for use with WebAssembly
parent: Development
grand_parent: Miscellaneous
nav_order: 2
---
# Configure SQLite for use with WebAssembly
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
```

View File

@@ -0,0 +1,114 @@
---
layout: default
title: Contributing
parent: Development
grand_parent: Miscellaneous
nav_order: 1
---
# Contributing
This document is a work-in-progress and will be expanded as time goes on. If you have any questions feel free to open a issue on GitHub.
Note: all instructions below are based on MacOS. If you are using a different operating system, you may need to adjust the commands accordingly.
## Getting Started
In order to contribute to this project follow these instructions to setup your local environment:
### 1. Clone the repository
```bash
git clone https://github.com/lanedirt/AliasVault.git
cd AliasVault
```
### 2. Copy pre-commit hook script to .git/hooks directory
{: .note }
All commits in this repo are required to contain a reference to a GitHub issue in the format of "your commit message (#123)" where "123" references the GitHub issue number.
The pre-commit hook script below will check the commit message before allowing the commit to proceed. If the commit message is invalid, the commit will be aborted.
```bash
# Copy the commit-msg hook script to the .git/hooks directory
cp .github/hooks/commit-msg .git/hooks/commit-msg
# Make the script executable
chmod +x .git/hooks/commit-msg
```
### 3. Install the latest version of .NET SDK 9
```bash
# Install .NET SDK 9
# On MacOS via brew:
brew install --cask dotnet-sdk
# On Windows via winget
winget install Microsoft.DotNet.SDK.9
```
### 4. Install dotnet CLI EF Tools
```bash
# Install dotnet EF tools globally
dotnet tool install --global dotnet-ef
# Include dotnet tools in your PATH
nano ~/.zshrc
# Add the following line to your .zshrc file
export PATH="$PATH:$HOME/.dotnet/tools"
# Start a new terminal and test that this command works:
dotnet ef
```
### 5. Install dev database
AliasVault uses PostgreSQL as its database. In order to run the project locally from Visual Studio / Rider you will need to install the dev database. You can do this by running the following command. This will start a separate PostgreSQL instance on port 5433 accessible via the `localhost:5433` address.
```bash
./install.sh configure-dev-db
```
After the database is running you can start the project from Visual Studio / Rider in run or debug mode and it should be able to connect to the dev database.
### 6. Run Tailwind CSS compiler when changing HTML files to update compiled CSS
```bash
# For Admin project (in the admin project directory)
npm run build:admin-css
# For Client project (in the client project directory)
npm run build:client-css
```
### 7. Install Playwright in order to locally run NUnit E2E (end-to-end) tests
```bash
# First install PowerShell for Mac (if you don't have it already)
brew install powershell/tap/powershell
# Install Playwright
dotnet tool install --global Microsoft.Playwright.CLI
# Run Playwright install script to download local browsers
# 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/net9.0/playwright.ps1 install
```
### 8. Create AliasVault.Client appsettings.Development.json
The WASM client 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:
```json
{
"ApiUrl": "http://localhost:5092",
"PrivateEmailDomains": ["example.tld"],
"SupportEmail": "support@example.tld",
"UseDebugEncryptionKey": "true",
"CryptographyOverrideType" : "Argon2Id",
"CryptographyOverrideSettings" : "{\"DegreeOfParallelism\":1,\"MemorySize\":1024,\"Iterations\":1}"
}
```
- **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.
- **CryptographyOverrideType**
- This setting allows overriding the default encryption type (Argon2id) with a different encryption type. This is useful for testing different encryption types without having to change code.
- **CryptographyOverrideSettings**
- This setting allows overriding the default encryption settings (Argon2id) with different settings. This is useful for testing different encryption settings without having to change code. The default Argon2id settings are defined in the project as `Utilities/Cryptography/Cryptography.Client/Defaults.cs`. These default settings are focused on security but NOT performance. Normally for key derivation purposes the slower/heavier the algorithm the better protection against attackers. For production builds this is what we want, however in case of automated testing or debugging extra performance can be gained by tweaking (lowering) these settings.
```

View File

@@ -3,13 +3,11 @@ layout: default
title: Enable WebAuthn
parent: Development
grand_parent: Miscellaneous
nav_order: 1
nav_order: 9
---
# WebAuthn
The webauthn implementation in order to quick unlock the vault requires the use of a FIDO2 authenticator.
This can be either the built-in browser authenticator or an external authenticator like a Yubikey.
Webauthn allows to quick unlock the vault. This can be either the built-in browser authenticator or an external authenticator like a Yubikey.
At the time of writing (2024-10-04), only some browsers support the required PRF extension. In order to make it work in Chrome, you need to enable the PRF extension in the browser settings.
@@ -19,3 +17,4 @@ At the time of writing (2024-10-04), only some browsers support the required PRF
2. Enable the `Experimental Web Platform features` flag.
3. Restart the browser.
4. Now it should be possible to use the built-in chrome password manager to unlock the vault.
5. Go to Menu -> Security Settings -> Quick Vault Unlock and enable it.

View File

@@ -0,0 +1,51 @@
---
layout: default
title: PostgreSQL Commands
parent: Development
grand_parent: Miscellaneous
nav_order: 2
---
# PostgreSQL Commands
## Backup database to file
To backup the database to a file, you can use the following command:
```bash
docker compose exec postgres pg_dump -U aliasvault aliasvault | gzip > aliasvault.sql.gz
```
## Import database from file
To drop the existing database and restore the database from a file, you can use the following command:
{: .warning }
Executing this command will drop the existing database and restore the database from the file. Make sure to have a backup of the existing database before running this command.
```bash
docker compose exec postgres psql -U aliasvault postgres -c "DROP DATABASE aliasvault;" && \
docker compose exec postgres psql -U aliasvault postgres -c "CREATE DATABASE aliasvault;" && \
gunzip < aliasvault.sql.gz | docker compose exec -iT postgres psql -U aliasvault aliasvault
```
## Change master password
By default during initial installation the PostgreSQL master password is set to a random string that is
stored in the `.env` file with the `POSTGRES_PASSWORD` variable.
If you wish to change the master password, you can do so by running the following command:
1. Open a terminal and navigate to the root of the AliasVault repository.
2. Run the following command to connect to the PostgreSQL container:
```bash
docker compose exec -it postgres psql -U aliasvault -d aliasvault
```
3. Once connected to the database, you can change the master password by running the following command:
```sql
ALTER USER aliasvault WITH PASSWORD 'new_password';
```
4. Press Enter to confirm the changes.
5. Exit the PostgreSQL shell by running `\q`.
6. Manually update the `.env` file variable `POSTGRES_PASSWORD` with the new password.
7. Restart the AliasVault containers by running the following command:
```bash
docker compose restart
```

View File

@@ -1,9 +1,9 @@
---
layout: default
title: 1. Run GitHub Actions Locally
title: Run GitHub Actions Locally
parent: Development
grand_parent: Miscellaneous
nav_order: 1
nav_order: 9
---
# Run GitHub Actions Locally

View File

@@ -19,4 +19,4 @@ To upgrade the AliasClientDb EF model, follow these steps:
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.
Make sure to manually test that the migration works as expected.

View File

@@ -0,0 +1,22 @@
---
layout: default
title: Upgrade the AliasServerDb EF model
parent: Development
grand_parent: Miscellaneous
nav_order: 3
---
# Upgrade the AliasServerDb EF model
The AliasServerDb EF model has migrations for both the SQLite and PostgreSQL databases. This means
that when you make changes to the EF model, you need to create migrations for both databases.
1. Make migration for PostgreSQL database:
```bash
dotnet ef migrations add InitialMigration --context AliasServerDbContextPostgresql --output-dir Migrations/PostgresqlMigrations
```
2. Make migration for SQLite database:
```bash
dotnet ef migrations add InitialMigration --context AliasServerDbContextSqlite --output-dir Migrations/SqliteMigrations
```

View File

@@ -10,10 +10,19 @@ nav_order: 1
Follow the steps in the checklist below to prepare a new release.
## Versioning
- [ ] Update ./src/Shared/AliasVault.Shared.Core/AppInfo.cs and update major/minor/patch to the new version. This version will be shown in the client and admin app footer.
- [ ] Update ./install.sh `@version` in header if the install script has changed. This allows the install script to self-update when running ./install.sh update command on default installations.
- [ ] Update ./install.sh `@version` in header if the install script has changed. This allows the install script to self-update when running the `./install.sh update` command on default installations.
## Docker Images
If docker containers have been added or removed:
- [ ] Verify that `.github/workflows/publish-docker-images.yml` contains references to all docker images that need to be published.
- [ ] Update `install.sh` and verify that the `images=()` array that takes care of pulling the images from the GitHub Container Registry is updated.
## Manual Testing (since v0.10.0+)
- [ ] Verify that the db migration from SQLite to PostgreSQL works. This needs to be tested manually until the SQLite support is removed. Test with: `./install.sh db-migrate` on an existing installation that has a SQLite database in `./database/AliasServerDb.sqlite`.
## Documentation
- [ ] Update /docs instructions if any changes have been made to the setup process
- [ ] Update README screenshots if applicable
- [ ] Update README current/upcoming features
Optional steps:
- [ ] Update /docs instructions if any changes have been made to the setup process

View File

@@ -1,12 +1,10 @@
#!/bin/bash
# @version 0.9.3
# @version 0.10.0
# Repository information used for downloading files and images from GitHub
REPO_OWNER="lanedirt"
REPO_NAME="AliasVault"
REPO_BRANCH="main"
GITHUB_RAW_URL_REPO="https://raw.githubusercontent.com/${REPO_OWNER}/${REPO_NAME}"
GITHUB_RAW_URL_REPO_BRANCH="$GITHUB_RAW_URL_REPO/$REPO_BRANCH"
GITHUB_CONTAINER_REGISTRY="ghcr.io/$(echo "$REPO_OWNER" | tr '[:upper:]' '[:lower:]')/$(echo "$REPO_NAME" | tr '[:upper:]' '[:lower:]')"
# Required files and directories
@@ -16,6 +14,7 @@ REQUIRED_DIRS=(
"certificates/letsencrypt"
"certificates/letsencrypt/www"
"database"
"database/postgres"
"logs"
"logs/msbuild"
)
@@ -38,23 +37,32 @@ show_usage() {
printf "Usage: $0 [COMMAND] [OPTIONS]\n"
printf "\n"
printf "Commands:\n"
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (recommended)\n"
printf " uninstall Uninstall AliasVault\n"
printf " update Update AliasVault to the latest version\n"
printf " update-installer Check and update install.sh script if newer version available\n"
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
printf " configure-email Configure email domains for receiving emails\n"
printf " start Start AliasVault containers\n"
printf " stop Stop AliasVault containers\n"
printf " restart Restart AliasVault containers\n"
printf " reset-password Reset admin password\n"
printf " build Build AliasVault from source (takes longer and requires sufficient specs)\n"
printf " install Install AliasVault by pulling pre-built images from GitHub Container Registry (recommended)\n"
printf " uninstall Uninstall AliasVault\n"
printf " update Update AliasVault to the latest version\n"
printf " update-installer Check and update install.sh script if newer version available\n"
printf " configure-ssl Configure SSL certificates (Let's Encrypt or self-signed)\n"
printf " configure-email Configure email domains for receiving emails\n"
printf " configure-registration Configure new account registration (enable or disable)\n"
printf " start Start AliasVault containers using remote images\n"
printf " stop Stop AliasVault containers using remote images\n"
printf " restart Restart AliasVault containers using remote images\n"
printf " reset-password Reset admin password\n"
printf " build [operation] Build AliasVault from source (takes longer and requires sufficient specs)\n"
printf " Optional operations: start|stop|restart (uses locally built images)\n"
printf "\n"
printf " db-export Export database to file\n"
printf " db-import Import database from file\n"
printf "\n"
printf " configure-dev-db Enable/disable development database (for local development only)\n"
printf " migrate-db Migrate data from SQLite to PostgreSQL when upgrading from a version prior to 0.10.0\n"
printf "\n"
printf "Options:\n"
printf " --verbose Show detailed output\n"
printf " -y, --yes Automatic yes to prompts (for uninstall)\n"
printf " -y, --yes Automatic yes to prompts\n"
printf " --help Show this help message\n"
printf "\n"
}
# Function to parse command line arguments
@@ -80,10 +88,23 @@ parse_args() {
shift
fi
;;
# Other commands remain unchanged
build|b)
COMMAND="build"
shift
# Check for additional operation argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
case $1 in
start|stop|restart)
COMMAND_ARG="$1"
shift
;;
*)
echo "Invalid build operation: $1"
echo "Valid operations are: start, stop, restart"
exit 1
;;
esac
fi
;;
uninstall|u)
COMMAND="uninstall"
@@ -101,6 +122,10 @@ parse_args() {
COMMAND="configure-email"
shift
;;
configure-registration|registration)
COMMAND="configure-registration"
shift
;;
start|s)
COMMAND="start"
shift
@@ -121,6 +146,27 @@ parse_args() {
COMMAND="update-installer"
shift
;;
configure-dev-db|dev-db)
COMMAND="configure-dev-db"
shift
# Check for direct option argument
if [ $# -gt 0 ] && [[ ! "$1" =~ ^- ]]; then
COMMAND_ARG="$1"
shift
fi
;;
migrate-db|migrate)
COMMAND="migrate-db"
shift
;;
db-export)
COMMAND="db-export"
shift
;;
db-import)
COMMAND="db-import"
shift
;;
--help)
show_usage
exit 0
@@ -164,19 +210,24 @@ main() {
print_logo
case $COMMAND in
"install")
handle_install "$COMMAND_ARG"
;;
"build")
handle_build
;;
"install")
handle_install "$COMMAND_ARG"
;;
"uninstall")
handle_uninstall
;;
"reset-password")
generate_admin_password
if [ $? -eq 0 ]; then
recreate_docker_containers
printf "${CYAN}> Restarting admin container...${NC}\n"
if [ "$VERBOSE" = true ]; then
$(get_docker_compose_command) up -d --force-recreate admin
else
$(get_docker_compose_command) up -d --force-recreate admin > /dev/null 2>&1
fi
print_password_reset_message
fi
;;
@@ -186,6 +237,9 @@ main() {
"configure-email")
handle_email_configuration
;;
"configure-registration")
handle_registration_configuration
;;
"start")
handle_start
;;
@@ -202,6 +256,18 @@ main() {
check_install_script_update
exit $?
;;
"configure-dev-db")
configure_dev_database
;;
"migrate-db")
handle_migrate_db
;;
"db-export")
handle_db_export
;;
"db-import")
handle_db_import
;;
esac
}
@@ -253,7 +319,7 @@ handle_docker_compose() {
fi
printf "\n ${CYAN}> docker-compose.yml downloaded successfully.${NC}\n"
else
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/blob/${version_tag}/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
printf "\n ${YELLOW}> Failed to download docker-compose.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.yml and place it in the root directory of AliasVault.${NC}\n"
exit 1
fi
@@ -262,22 +328,60 @@ handle_docker_compose() {
if curl -sSf "${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.letsencrypt.yml" -o "docker-compose.letsencrypt.yml" > /dev/null 2>&1; then
printf "\n ${CYAN}> docker-compose.letsencrypt.yml downloaded successfully.${NC}\n"
else
printf "\n ${YELLOW}> Failed to download docker-compose.letsencrypt.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/blob/${version_tag}/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
printf "\n ${YELLOW}> Failed to download docker-compose.letsencrypt.yml, please check your internet connection and try again. Alternatively, you can download it manually from ${GITHUB_RAW_URL_REPO}/${version_tag}/docker-compose.letsencrypt.yml and place it in the root directory of AliasVault.${NC}\n"
exit 1
fi
return 0
}
# Function to check and update install.sh for specific version
check_install_script_version() {
local target_version="$1"
printf "${CYAN}> Checking install script version for ${target_version}...${NC}\n"
# Get remote install.sh for target version
if ! curl -sSf "${GITHUB_RAW_URL_REPO}/${target_version}/install.sh" -o "install.sh.tmp"; then
printf "${RED}> Failed to check install script version. Continuing with current version.${NC}\n"
rm -f install.sh.tmp
return 1
fi
# Get versions
local current_version=$(extract_version "install.sh")
local target_script_version=$(extract_version "install.sh.tmp")
# Check if versions could be extracted
if [ -z "$current_version" ] || [ -z "$target_script_version" ]; then
printf "${YELLOW}> Could not determine script versions. Falling back to file comparison...${NC}\n"
if ! cmp -s "install.sh" "install.sh.tmp"; then
printf "${YELLOW}> Install script needs updating to match version ${target_version}${NC}\n"
return 2
fi
else
printf "${CYAN}> Current install script version: ${current_version}${NC}\n"
printf "${CYAN}> Target install script version: ${target_script_version}${NC}\n"
if [ "$current_version" != "$target_script_version" ]; then
printf "${YELLOW}> Install script needs updating to match version ${target_version}${NC}\n"
return 2
fi
fi
printf "${GREEN}> Install script is up to date for version ${target_version}.${NC}\n"
rm -f install.sh.tmp
return 0
}
# Function to print the logo
print_logo() {
printf "${MAGENTA}"
printf " _ _ _ __ __ _ _ \n"
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n"
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n"
printf " / ___ \| | | (_| \__ \ \ / / (_| | |_| | | |_ \n"
printf "/_/ \_\_|_|\__,_|___/ \/ \__,__|\__,_|_|\__|\n"
printf "${NC}\n"
printf "${MAGENTA}" >&2
printf " _ _ _ __ __ _ _ \n" >&2
printf " / \ | (_) __ _ ___ \ \ / /_ _ _ _| | |_\n" >&2
printf " / _ \ | | |/ _\` / __| \ \/\/ / _\` | | | | | __|\n" >&2
printf " / ___ \| | | (_| \__ \ \ / / (_| | |_| | | |_ \n" >&2
printf "/_/ \_\_|_|\__,_|___/ \/ \__,__|\__,_|_|\__|\n" >&2
printf "${NC}\n" >&2
}
# Function to create .env file
@@ -330,6 +434,30 @@ populate_data_protection_cert_pass() {
fi
}
populate_postgres_credentials() {
printf "${CYAN}> Checking Postgres credentials...${NC}\n"
if ! grep -q "^POSTGRES_DB=" "$ENV_FILE" || [ -z "$(grep "^POSTGRES_DB=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "POSTGRES_DB" "aliasvault"
else
printf " ${GREEN}> POSTGRES_DB already exists.${NC}\n"
fi
if ! grep -q "^POSTGRES_USER=" "$ENV_FILE" || [ -z "$(grep "^POSTGRES_USER=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "POSTGRES_USER" "aliasvault"
else
printf " ${GREEN}> POSTGRES_USER already exists.${NC}\n"
fi
if ! grep -q "^POSTGRES_PASSWORD=" "$ENV_FILE" || [ -z "$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
# Generate a strong random password with 32 characters
POSTGRES_PASS=$(openssl rand -base64 32)
update_env_var "POSTGRES_PASSWORD" "$POSTGRES_PASS"
else
printf " ${GREEN}> POSTGRES_PASSWORD already exists.${NC}\n"
fi
}
set_private_email_domains() {
printf "${CYAN}> Checking PRIVATE_EMAIL_DOMAINS...${NC}\n"
if ! grep -q "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" || [ -z "$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
@@ -338,7 +466,7 @@ set_private_email_domains() {
private_email_domains=$(grep "^PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE" | cut -d '=' -f2)
if [ "$private_email_domains" = "DISABLED.TLD" ]; then
printf " ${RED}Email server is disabled.${NC} To enable use ./install.sh configure-email command.\n"
printf " ${GREEN}> Email server is disabled. To enable use ./install.sh configure-email command.${NC}\n"
else
printf " ${GREEN}> PRIVATE_EMAIL_DOMAINS already exists. Email server is enabled.${NC}\n"
fi
@@ -363,6 +491,15 @@ set_support_email() {
fi
}
set_public_registration() {
printf "${CYAN}> Checking PUBLIC_REGISTRATION_ENABLED...${NC}\n"
if ! grep -q "^PUBLIC_REGISTRATION_ENABLED=" "$ENV_FILE" || [ -z "$(grep "^PUBLIC_REGISTRATION_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "PUBLIC_REGISTRATION_ENABLED" "true"
else
printf " ${GREEN}> PUBLIC_REGISTRATION_ENABLED already exists.${NC}\n"
fi
}
# Function to generate admin password
generate_admin_password() {
printf "${CYAN}> Generating admin password...${NC}\n"
@@ -496,13 +633,14 @@ print_success_message() {
# Function to recreate (restart) Docker containers
recreate_docker_containers() {
printf "${CYAN}> Recreating Docker containers...${NC}\n"
printf "${CYAN}> (Re)creating Docker containers...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose up -d --force-recreate
$(get_docker_compose_command) up -d --force-recreate
else
docker compose up -d --force-recreate > /dev/null 2>&1
$(get_docker_compose_command) up -d --force-recreate > /dev/null 2>&1
fi
printf "${GREEN}> Docker containers recreated.${NC}\n"
printf "${GREEN}> Docker containers (re)created successfully.${NC}\n"
}
# Function to print password reset success message
@@ -510,7 +648,7 @@ print_password_reset_message() {
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
printf "\n"
printf "${GREEN}The admin password is successfully reset, see the output above. You can now login to the admin panel using this new password.${NC}\n"
printf "${GREEN}The admin password has been successfully reset, see the output above.${NC}\n"
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
printf "\n"
@@ -521,7 +659,7 @@ get_docker_compose_command() {
local base_command="docker compose -f docker-compose.yml"
# Check if using build configuration
if [ "$1" = "build" ]; then
if grep -q "^DEPLOYMENT_MODE=build" "$ENV_FILE" 2>/dev/null; then
base_command="$base_command -f docker-compose.build.yml"
fi
@@ -533,6 +671,80 @@ get_docker_compose_command() {
echo "$base_command"
}
# Add this new function for handling registration configuration
handle_registration_configuration() {
printf "${YELLOW}+++ Public Registration Configuration +++${NC}\n"
printf "\n"
# Check if AliasVault is installed
if [ ! -f "docker-compose.yml" ]; then
printf "${RED}Error: AliasVault must be installed first.${NC}\n"
exit 1
fi
# Get current registration setting
CURRENT_SETTING=$(grep "^PUBLIC_REGISTRATION_ENABLED=" "$ENV_FILE" | cut -d '=' -f2)
printf "${CYAN}About Public Registration:${NC}\n"
printf "Public registration allows new users to create their own accounts on your AliasVault instance.\n"
printf "When disabled, no new accounts can be created.\n"
printf "\n"
printf "${CYAN}Current Configuration:${NC}\n"
if [ "$CURRENT_SETTING" = "true" ]; then
printf "Public Registration: ${GREEN}Enabled${NC}\n"
else
printf "Public Registration: ${RED}Disabled${NC}\n"
fi
printf "\n"
printf "Options:\n"
printf "1) Enable public registration\n"
printf "2) Disable public registration\n"
printf "3) Cancel\n"
printf "\n"
read -p "Select an option [1-3]: " reg_option
case $reg_option in
1)
update_env_var "PUBLIC_REGISTRATION_ENABLED" "true"
printf "${GREEN}> Public registration has been enabled.${NC}\n"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
;;
2)
update_env_var "PUBLIC_REGISTRATION_ENABLED" "false"
printf "${YELLOW}> Public registration has been disabled.${NC}\n"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
handle_restart
;;
3)
printf "${YELLOW}Registration configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to handle initial installation or reinstallation
handle_install() {
local specified_version="$1"
@@ -570,8 +782,13 @@ handle_install() {
# Function to handle build
handle_build() {
printf "${YELLOW}+++ Building AliasVault from source +++${NC}\n"
# Set deployment mode to build to ensure container lifecycle uses build configuration
set_deployment_mode "build"
printf "\n"
# Initialize workspace which makes sure all required directories and files exist
initialize_workspace
# Check for required build files
if [ ! -f "docker-compose.build.yml" ] || [ ! -d "src" ]; then
printf "${RED}Error: Required files for building from source are missing.${NC}\n"
@@ -590,12 +807,14 @@ handle_build() {
# Initialize environment with proper error handling
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
populate_jwt_key || { printf "${RED}> Failed to set JWT key${NC}\n"; exit 1; }
populate_data_protection_cert_pass || { printf "${RED}> Failed to set certificate password${NC}\n"; exit 1; }
populate_postgres_credentials || { printf "${RED}> Failed to set PostgreSQL credentials${NC}\n"; exit 1; }
set_private_email_domains || { printf "${RED}> Failed to set email domains${NC}\n"; exit 1; }
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
set_default_ports || { printf "${RED}> Failed to set default ports${NC}\n"; exit 1; }
set_public_registration || { printf "${RED}> Failed to set public registration${NC}\n"; exit 1; }
# Only generate admin password if not already set
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
@@ -630,17 +849,9 @@ handle_build() {
printf "\n${GREEN}> Docker Compose stack built successfully.${NC}\n"
printf "${CYAN}> Starting Docker Compose stack...${NC}\n"
if [ "$VERBOSE" = true ]; then
$(get_docker_compose_command "build") up -d --force-recreate || {
printf "${RED}> Failed to start Docker Compose stack${NC}\n"
exit 1
}
else
$(get_docker_compose_command "build") up -d --force-recreate > /dev/null 2>&1 || {
printf "${RED}> Failed to start Docker Compose stack${NC}\n"
exit 1
}
fi
recreate_docker_containers
printf "${GREEN}> Docker Compose stack started successfully.${NC}\n"
# Only show success message if we made it here without errors
@@ -785,7 +996,6 @@ handle_ssl_configuration() {
esac
}
# Function to handle email server configuration
# Function to handle email server configuration
handle_email_configuration() {
# Setup trap for Ctrl+C and other interrupts
@@ -852,14 +1062,6 @@ handle_email_configuration() {
fi
done
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Continue with restart? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Configuration cancelled.${NC}\n"
exit 0
fi
# Update .env file and restart
if ! update_env_var "PRIVATE_EMAIL_DOMAINS" "$new_domains"; then
printf "${RED}Failed to update configuration.${NC}\n"
@@ -867,6 +1069,15 @@ handle_email_configuration() {
fi
printf "${GREEN}Email server configuration updated${NC}\n"
printf "\n${YELLOW}Warning: Docker containers need to be restarted to apply these changes.${NC}\n"
read -p "Restart now? (y/n): " restart_confirm
if [ "$restart_confirm" != "y" ] && [ "$restart_confirm" != "Y" ]; then
printf "${YELLOW}Please restart manually to apply the changes.${NC}\n"
exit 0
fi
printf "Restarting AliasVault services...\n"
if ! handle_restart; then
@@ -1166,8 +1377,15 @@ compare_versions() {
check_install_script_update() {
printf "${CYAN}> Checking for install script updates...${NC}\n"
# Download latest install.sh to temporary file
if ! curl -sSf "${GITHUB_RAW_URL_REPO_BRANCH}/install.sh" -o "install.sh.tmp"; then
# Get latest release version
local latest_version=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$latest_version" ]; then
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
return 1
fi
if ! curl -sSf "${GITHUB_RAW_URL_REPO}/${latest_version}/install.sh" -o "install.sh.tmp"; then
printf "${RED}> Failed to check for install script updates. Continuing with current version.${NC}\n"
rm -f install.sh.tmp
return 1
@@ -1260,23 +1478,61 @@ handle_install_version() {
fi
printf "${YELLOW}+++ Installing AliasVault ${target_version} +++${NC}\n"
# Set deployment mode to install to ensure container lifecycle uses install configuration
set_deployment_mode "install"
printf "\n"
# Initialize workspace which makes sure all required directories and files exist
initialize_workspace
# Check if install script needs updating for this version
check_install_script_version "$target_version"
local check_result=$?
if [ $check_result -eq 2 ]; then
if [ "$FORCE_YES" = true ]; then
printf "${CYAN}> Updating install script to match version ${target_version}...${NC}\n"
else
printf "${YELLOW}> A different version of the install script is required for installing version ${target_version}.${NC}\n"
read -p "Would you like to self-update the install script before proceeding? [Y/n]: " reply
if [[ $reply =~ ^[Nn]$ ]]; then
printf "${YELLOW}> Continuing with current install script version.${NC}\n"
rm -f install.sh.tmp
fi
fi
if [ "$FORCE_YES" = true ] || [[ ! $reply =~ ^[Nn]$ ]]; then
# Create backup of current script
cp "install.sh" "install.sh.backup"
if mv "install.sh.tmp" "install.sh"; then
chmod +x "install.sh"
printf "${GREEN}> Install script updated successfully.${NC}\n"
printf "${GREEN}> Backup of previous version saved as install.sh.backup${NC}\n"
printf "${YELLOW}> Please run the same install command again to continue with the installation.${NC}\n"
exit 0
else
printf "${RED}> Failed to update install script. Continuing with current version.${NC}\n"
mv "install.sh.backup" "install.sh"
rm -f install.sh.tmp
fi
fi
fi
# Update docker-compose files with correct version so we pull the correct images
handle_docker_compose "$target_version"
# Initialize environment
create_env_file || { printf "${RED}> Failed to create .env file${NC}\n"; exit 1; }
populate_hostname || { printf "${RED}> Failed to set hostname${NC}\n"; exit 1; }
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
populate_jwt_key || { printf "${RED}> Failed to set JWT key${NC}\n"; exit 1; }
populate_data_protection_cert_pass || { printf "${RED}> Failed to set certificate password${NC}\n"; exit 1; }
populate_postgres_credentials || { printf "${RED}> Failed to set PostgreSQL credentials${NC}\n"; exit 1; }
set_private_email_domains || { printf "${RED}> Failed to set email domains${NC}\n"; exit 1; }
set_smtp_tls_enabled || { printf "${RED}> Failed to set SMTP TLS${NC}\n"; exit 1; }
set_support_email || { printf "${RED}> Failed to set support email${NC}\n"; exit 1; }
set_default_ports || { printf "${RED}> Failed to set default ports${NC}\n"; exit 1; }
set_public_registration || { printf "${RED}> Failed to set public registration${NC}\n"; exit 1; }
# Only generate admin password if not already set
if ! grep -q "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" || [ -z "$(grep "^ADMIN_PASSWORD_HASH=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
@@ -1290,6 +1546,7 @@ handle_install_version() {
printf "${CYAN}> Installing version: ${target_version}${NC}\n"
images=(
"${GITHUB_CONTAINER_REGISTRY}-postgres:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-reverse-proxy:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-api:${target_version}"
"${GITHUB_CONTAINER_REGISTRY}-client:${target_version}"
@@ -1301,9 +1558,9 @@ handle_install_version() {
for image in "${images[@]}"; do
printf "${CYAN}> Pulling $image...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker pull $image || { printf "${RED}> Failed to pull image: $image${NC}\n"; exit 1; }
docker pull $image || printf "${YELLOW}> Warning: Failed to pull image: $image - continuing anyway${NC}\n"
else
docker pull $image > /dev/null 2>&1 || { printf "${RED}> Failed to pull image: $image${NC}\n"; exit 1; }
docker pull $image > /dev/null 2>&1 || printf "${YELLOW}> Warning: Failed to pull image: $image - continuing anyway${NC}\n"
fi
done
@@ -1313,10 +1570,367 @@ handle_install_version() {
# Start containers
printf "\n${YELLOW}+++ Starting services +++${NC}\n"
printf "\n"
recreate_docker_containers
if [ "$VERBOSE" = true ]; then
docker compose up -d --force-recreate
else
docker compose up -d --force-recreate > /dev/null 2>&1
fi
printf "${GREEN}> Docker containers recreated.${NC}\n"
# Only show success message if we made it here without errors
print_success_message
}
# Function to handle development database configuration
configure_dev_database() {
printf "${YELLOW}+++ Development Database Configuration +++${NC}\n"
printf "\n"
if [ ! -f "docker-compose.dev.yml" ]; then
printf "${RED}> The docker-compose.dev.yml file is missing. This file is required to start the development database. Please checkout the full GitHub repository and try again.${NC}\n"
return 1
fi
# Check if direct option was provided
if [ -n "$COMMAND_ARG" ]; then
case $COMMAND_ARG in
1|start)
if docker compose -f docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
printf "${YELLOW}> Development database is already running.${NC}\n"
else
printf "${CYAN}> Starting development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml up -d --wait --wait-timeout 60
printf "${GREEN}> Development database started successfully.${NC}\n"
fi
print_dev_db_details
return
;;
0|stop)
if ! docker compose -f docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
printf "${YELLOW}> Development database is already stopped.${NC}\n"
else
printf "${CYAN}> Stopping development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml down
printf "${GREEN}> Development database stopped successfully.${NC}\n"
fi
return
;;
esac
fi
# Check current status
if docker compose -f docker-compose.dev.yml -p aliasvault-dev ps --status running 2>/dev/null | grep -q postgres-dev; then
DEV_DB_STATUS="running"
else
DEV_DB_STATUS="stopped"
fi
printf "${CYAN}About Development Database:${NC}\n"
printf "A separate PostgreSQL instance for development purposes that:\n"
printf " - Runs on port 5433 (to avoid conflicts)\n"
printf " - Uses simple credentials (password: 'password')\n"
printf " - Stores data separately from production\n"
printf "\n"
printf "${CYAN}Current Status:${NC}\n"
if [ "$DEV_DB_STATUS" = "running" ]; then
printf "Development Database: ${GREEN}Running${NC}\n"
else
printf "Development Database: ${YELLOW}Stopped${NC}\n"
fi
printf "\n"
printf "Options:\n"
printf "1) Start development database\n"
printf "2) Stop development database\n"
printf "3) View connection details\n"
printf "4) Cancel\n"
printf "\n"
read -p "Select an option [1-4]: " dev_db_option
case $dev_db_option in
1)
if [ "$DEV_DB_STATUS" = "running" ]; then
printf "${YELLOW}> Development database is already running.${NC}\n"
else
printf "${CYAN}> Starting development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml up -d --wait --wait-timeout 60
printf "${GREEN}> Development database started successfully.${NC}\n"
fi
print_dev_db_details
;;
2)
if [ "$DEV_DB_STATUS" = "stopped" ]; then
printf "${YELLOW}> Development database is already stopped.${NC}\n"
else
printf "${CYAN}> Stopping development database...${NC}\n"
docker compose -p aliasvault-dev -f docker-compose.dev.yml down
printf "${GREEN}> Development database stopped successfully.${NC}\n"
fi
;;
3)
print_dev_db_details
;;
4)
printf "${YELLOW}Configuration cancelled.${NC}\n"
exit 0
;;
*)
printf "${RED}Invalid option selected.${NC}\n"
exit 1
;;
esac
}
# Function to print development database connection details
print_dev_db_details() {
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
printf "\n"
printf "${CYAN}Development Database Connection Details:${NC}\n"
printf "Host: localhost\n"
printf "Port: 5433\n"
printf "Database: aliasvault\n"
printf "Username: aliasvault\n"
printf "Password: password\n"
printf "\n"
printf "Connection string:\n"
printf "Host=localhost;Port=5433;Database=aliasvault;Username=aliasvault;Password=password\n"
printf "\n"
printf "${MAGENTA}=========================================================${NC}\n"
}
# Function to handle database migration. This is a one-time operation necessary when upgrading from <= 0.9.x to 0.10.0+ and only needs to be run once.
handle_migrate_db() {
printf "${YELLOW}+++ Database Migration Tool +++${NC}\n"
printf "\n"
# Check for old SQLite database
SQLITE_DB="database/AliasServerDb.sqlite"
if [ ! -f "$SQLITE_DB" ]; then
printf "${RED}Error: SQLite database not found at ${SQLITE_DB}${NC}\n"
exit 1
fi
# Get the absolute path of the SQLite database
SQLITE_DB_ABS=$(realpath "$SQLITE_DB")
SQLITE_DB_DIR=$(dirname "$SQLITE_DB_ABS")
SQLITE_DB_NAME=$(basename "$SQLITE_DB_ABS")
# Get PostgreSQL password from .env file
POSTGRES_PASSWORD=$(grep "^POSTGRES_PASSWORD=" "$ENV_FILE" | cut -d= -f2-)
if [ -z "$POSTGRES_PASSWORD" ]; then
printf "${RED}Error: POSTGRES_PASSWORD not found in .env file${NC}\n"
exit 1
fi
# Get network name in lowercase
NETWORK_NAME="$(pwd | xargs basename)_default"
NETWORK_NAME=$(echo "$NETWORK_NAME" | tr '[:upper:]' '[:lower:]')
printf "\n${YELLOW}Warning: This will migrate data from your SQLite database to PostgreSQL.${NC}\n"
printf "\n"
printf "This is a one-time operation necessary when upgrading from <= 0.9.x to 0.10.0+ and only needs to be run once.\n"
printf "\n"
printf "Source database: ${CYAN}${SQLITE_DB_ABS}${NC}\n"
printf "Target: PostgreSQL database (using connection string from docker-compose.yml)\n"
printf "Make sure you have backed up your data before proceeding.\n"
printf "\n${RED}WARNING: This operation will DELETE ALL EXISTING DATA in the PostgreSQL database.${NC}\n"
printf "${RED}Only proceed if you understand that any current PostgreSQL data will be permanently lost.${NC}\n"
printf "${RED}This operation will stop all services and restart them after the migration is complete.${NC}\n"
printf "\n"
read -p "Continue with migration? [y/N]: " confirm
if [[ ! $confirm =~ ^[Yy]$ ]]; then
printf "${YELLOW}Migration cancelled.${NC}\n"
exit 0
fi
printf "${CYAN}> Stopping services to ensure database is not in use...${NC}\n"
docker compose stop api admin task-runner smtp
if ! docker pull ${GITHUB_CONTAINER_REGISTRY}-installcli:0.10.0 > /dev/null 2>&1; then
printf "${YELLOW}> Pre-built image not found, building locally...${NC}"
if [ "$VERBOSE" = true ]; then
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile .
else
(
docker build -t installcli -f src/Utilities/AliasVault.InstallCli/Dockerfile . > install_build_output.log 2>&1 &
BUILD_PID=$!
while kill -0 $BUILD_PID 2>/dev/null; do
printf "."
sleep 1
done
printf "\n"
wait $BUILD_PID
BUILD_EXIT_CODE=$?
if [ $BUILD_EXIT_CODE -ne 0 ]; then
printf "\n${RED}> Error building Docker image. Check install_build_output.log for details.${NC}\n"
exit $BUILD_EXIT_CODE
fi
)
fi
# Run migration with volume mount and connection string
docker run --rm \
--network="${NETWORK_NAME}" \
-v "${SQLITE_DB_DIR}:/sqlite" \
installcli migrate-sqlite "/sqlite/${SQLITE_DB_NAME}" "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}"
else
# Run migration with volume mount using pre-built image
docker run --rm \
--network="${NETWORK_NAME}" \
-v "${SQLITE_DB_DIR}:/sqlite" \
${GITHUB_CONTAINER_REGISTRY}-installcli:0.10.0 migrate-sqlite "/sqlite/${SQLITE_DB_NAME}" "Host=postgres;Database=aliasvault;Username=aliasvault;Password=${POSTGRES_PASSWORD}"
fi
# Starting services again
printf "${CYAN}> Starting services...${NC}\n"
docker compose start api admin task-runner smtp reverse-proxy
printf "${GREEN}> Check migration output above for details.${NC}\n"
}
# Function to set deployment mode in .env
set_deployment_mode() {
local mode=$1
if [ "$mode" != "build" ] && [ "$mode" != "install" ]; then
printf "${RED}Invalid deployment mode: $mode${NC}\n"
exit 1
fi
update_env_var "DEPLOYMENT_MODE" "$mode"
}
# Function to handle database export
handle_db_export() {
# Print logo and headers to stderr
printf "${YELLOW}+++ Exporting Database +++${NC}\n" >&2
printf "\n" >&2
# Check if output redirection is present
if [ -t 1 ]; then
printf "${RED}Error: Output redirection is required.${NC}\n" >&2
printf "Usage: ./install.sh db-export > backup.sql.gz\n" >&2
printf "\n" >&2
printf "Example:\n" >&2
printf " ./install.sh db-export > my_backup_$(date +%Y%m%d).sql.gz\n" >&2
exit 1
fi
# Check if containers are running
if ! docker compose ps --quiet 2>/dev/null | grep -q .; then
printf "${RED}Error: AliasVault containers are not running. Start them first with: ./install.sh start${NC}\n" >&2
exit 1
fi
# Check if postgres container is healthy
if ! docker compose ps postgres | grep -q "healthy"; then
printf "${RED}Error: PostgreSQL container is not healthy. Please check the logs with: docker compose logs postgres${NC}\n" >&2
exit 1
fi
printf "${CYAN}> Exporting database...${NC}\n" >&2
# Only the actual pg_dump output goes to stdout, everything else to stderr
docker compose exec postgres pg_dump -U aliasvault aliasvault | gzip
if [ $? -eq 0 ]; then
printf "${GREEN}> Database exported successfully.${NC}\n" >&2
else
printf "${RED}> Failed to export database.${NC}\n" >&2
exit 1
fi
}
# Function to handle database import
handle_db_import() {
printf "${YELLOW}+++ Importing Database +++${NC}\n"
# Check if containers are running
if ! docker compose ps postgres | grep -q "healthy"; then
printf "${RED}Error: PostgreSQL container is not healthy.${NC}\n"
exit 1
fi
# Check if we're getting input from a pipe
if [ -t 0 ]; then
printf "${RED}Error: No input file provided${NC}\n"
printf "Usage: ./install.sh db-import < backup.sql.gz\n"
exit 1
fi
# Save stdin to file descriptor 3
exec 3<&0
printf "${RED}Warning: This will DELETE ALL EXISTING DATA in the database.${NC}\n"
if [ "$FORCE_YES" != true ]; then
# Use /dev/tty to read from terminal even when stdin is redirected
if [ -t 1 ] && [ -t 2 ] && [ -e /dev/tty ]; then
# Temporarily switch stdin to tty for confirmation
exec < /dev/tty
read -p "Continue? [y/N]: " confirm
# Switch back to original stdin
exec 0<&3
if [[ ! $confirm =~ ^[Yy]$ ]]; then
exec 3<&- # Close fd 3
exit 1
fi
else
printf "${RED}Error: Cannot read confirmation from terminal. Use -y flag to bypass confirmation.${NC}\n"
exec 3<&- # Close fd 3
exit 1
fi
fi
printf "${CYAN}> Stopping dependent services...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose stop api admin task-runner smtp
else
docker compose stop api admin task-runner smtp > /dev/null 2>&1
fi
printf "${CYAN}> Importing database...${NC}\n"
# Create a temporary file to verify the gzip input
temp_file=$(mktemp)
cat <&3 > "$temp_file" # Read from fd 3 instead of stdin
exec 3<&- # Close fd 3
if ! gzip -t "$temp_file" 2>/dev/null; then
printf "${RED}Error: Input is not a valid gzip file${NC}\n"
rm "$temp_file"
exit 1
fi
if [ "$VERBOSE" = true ]; then
# Proceed with import
docker compose exec -T postgres psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" && \
docker compose exec -T postgres psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" && \
docker compose exec -T postgres psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" && \
gunzip -c "$temp_file" | docker compose exec -T postgres psql -U aliasvault aliasvault
else
# Suppress all output except errors
docker compose exec -T postgres psql -U aliasvault postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'aliasvault' AND pid <> pg_backend_pid();" > /dev/null 2>&1 && \
docker compose exec -T postgres psql -U aliasvault postgres -c "DROP DATABASE IF EXISTS aliasvault;" > /dev/null 2>&1 && \
docker compose exec -T postgres psql -U aliasvault postgres -c "CREATE DATABASE aliasvault OWNER aliasvault;" > /dev/null 2>&1 && \
gunzip -c "$temp_file" | docker compose exec -T postgres psql -U aliasvault aliasvault > /dev/null 2>&1
fi
import_status=$?
rm "$temp_file"
if [ $import_status -eq 0 ]; then
printf "${GREEN}> Database imported successfully.${NC}\n"
printf "${CYAN}> Starting services...${NC}\n"
if [ "$VERBOSE" = true ]; then
docker compose restart api admin task-runner smtp reverse-proxy
else
docker compose restart api admin task-runner smtp reverse-proxy > /dev/null 2>&1
fi
else
printf "${RED}> Import failed. Please check that your backup file is valid.${NC}\n"
exit 1
fi
}
main "$@"

View File

@@ -64,7 +64,6 @@
var user = await UserManager.FindByNameAsync(Input.UserName);
if (user == null)
{
await AuthLoggingService.LogAuthEventFailAsync(Input.UserName, AuthEventType.Login, AuthFailureReason.InvalidUsername);
ServerValidationErrors.AddError("Error: Invalid login attempt.");
return;
@@ -75,7 +74,7 @@
{
await AuthLoggingService.LogAuthEventSuccessAsync(Input.UserName, AuthEventType.Login);
Logger.LogInformation("User logged in.");
NavigationService.RedirectTo(ReturnUrl ?? "/");
NavigationService.RedirectTo(ReturnUrl ?? "./");
}
else if (result.RequiresTwoFactor)
{

View File

@@ -8,11 +8,13 @@
protected override async Task OnInitializedAsync()
{
// Sign out the user.
// NOTE: the try/catch below is a workaround for the issue that the sign out does not work when
// NOTE: the try/catch below is a workaround for the issue that the sign-out does not work when
// the server session is already started.
try
{
await UserService.LoadCurrentUserAsync();
var username = UserService.User().UserName;
try
{
await SignInManager.SignOutAsync();
@@ -20,12 +22,12 @@
await AuthLoggingService.LogAuthEventSuccessAsync(username!, AuthEventType.Logout);
// Redirect to the home page with hard refresh.
NavigationService.RedirectTo("/", true);
NavigationService.RedirectTo("./", true);
}
catch
{
// Hard refresh current page if sign out fails. When an interactive server session is already started
// the sign out will fail because it tries to mutate cookies which is only possible when the server
// the sign-out will fail because it tries to mutate cookies which is only possible when the server
// session is not started yet.
await AuthLoggingService.LogAuthEventSuccessAsync(username!, AuthEventType.Logout);
NavigationService.RedirectTo(NavigationService.Uri, true);

View File

@@ -102,7 +102,7 @@
/// </summary>
private static bool IsHeartbeatValid(DateTime lastHeartbeat)
{
return DateTime.Now <= lastHeartbeat.AddMinutes(5);
return DateTime.UtcNow <= lastHeartbeat.AddMinutes(5);
}
/// <summary>
@@ -171,7 +171,7 @@
try
{
InitInProgress = true;
var dbContext = await DbContextFactory.CreateDbContextAsync();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
ServiceStatus = await dbContext.WorkerServiceStatuses.ToListAsync();
foreach (var service in Services)
@@ -197,7 +197,7 @@
/// </summary>
private async Task<bool> UpdateServiceStatus(string serviceName, bool newStatus)
{
var dbContext = await DbContextFactory.CreateDbContextAsync();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var entry = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstOrDefaultAsync();
if (entry != null)
{
@@ -205,16 +205,16 @@
entry.DesiredStatus = newDesiredStatus;
await dbContext.SaveChangesAsync();
var timeout = DateTime.Now.AddSeconds(30);
var timeout = DateTime.UtcNow.AddSeconds(30);
while (true)
{
if (DateTime.Now > timeout)
if (DateTime.UtcNow > timeout)
{
return false;
}
dbContext = await DbContextFactory.CreateDbContextAsync();
var check = await dbContext.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstAsync();
await using var dbContextInner = await DbContextFactory.CreateDbContextAsync();
var check = await dbContextInner.WorkerServiceStatuses.Where(x => x.ServiceName == serviceName).FirstAsync();
if (check.CurrentStatus == newDesiredStatus)
{
return true;

View File

@@ -32,6 +32,11 @@ public class UserViewModel
/// </summary>
public bool TwoFactorEnabled { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user is blocked.
/// </summary>
public bool Blocked { get; set; }
/// <summary>
/// Gets or sets the vault count.
/// </summary>

View File

@@ -2,7 +2,6 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Identity
@inject UserManager<AdminUser> UserManager
@inject ILogger<ChangePassword> Logger
@@ -41,15 +40,13 @@
private async Task OnValidSubmitAsync()
{
var changePasswordResult = await UserManager.ChangePasswordAsync(UserService.User(), Input.OldPassword, Input.NewPassword);
var user = UserService.User();
user.LastPasswordChanged = DateTime.UtcNow;
await UserService.UpdateUserAsync(user);
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
// Clear the password fields
Input.OldPassword = "";
Input.NewPassword = "";
Input.ConfirmPassword = "";
var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword);
if (!changePasswordResult.Succeeded)
{
@@ -57,10 +54,15 @@
return;
}
user.LastPasswordChanged = DateTime.UtcNow;
await UserManager.UpdateAsync(user);
Input.OldPassword = "";
Input.NewPassword = "";
Input.ConfirmPassword = "";
Logger.LogInformation("User changed their password successfully.");
GlobalNotificationService.AddSuccessMessage("Your password has been changed.");
NavigationService.RedirectToCurrentPage();
}
@@ -82,5 +84,4 @@
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; } = "";
}
}

View File

@@ -31,7 +31,13 @@
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
if (!await UserManager.GetTwoFactorEnabledAsync(UserService.User()))
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
if (!await UserManager.GetTwoFactorEnabledAsync(user))
{
throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled.");
}
@@ -39,7 +45,13 @@
private async Task OnSubmitAsync()
{
var disable2FaResult = await UserManager.SetTwoFactorEnabledAsync(UserService.User(), false);
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
var disable2FaResult = await UserManager.SetTwoFactorEnabledAsync(user, false);
if (!disable2FaResult.Succeeded)
{
await AuthLoggingService.LogAuthEventFailAsync(UserService.User().UserName!, AuthEventType.TwoFactorAuthDisable, AuthFailureReason.Unknown);

View File

@@ -13,6 +13,12 @@
<LayoutPageTitle>Configure authenticator app</LayoutPageTitle>
@if (_isLoading)
{
<LoadingIndicator />
return;
}
@if (RecoveryCodes is not null)
{
<ShowRecoveryCodes RecoveryCodes="RecoveryCodes.ToArray()"/>
@@ -69,15 +75,20 @@ else
private string? SharedKey { get; set; }
private string? AuthenticatorUri { get; set; }
private IEnumerable<string>? RecoveryCodes { get; set; }
private bool _isLoading = true;
[SupplyParameterFromForm] private InputModel Input { get; set; } = new();
/// <inheritdoc />
protected override async Task OnInitializedAsync()
/// <inheritdoc/>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnInitializedAsync();
await LoadSharedKeyAndQrCodeUriAsync(UserService.User());
await JsInvokeService.RetryInvokeAsync("generateQrCode", TimeSpan.Zero, 5, "authenticator-uri");
if (firstRender)
{
await LoadSharedKeyAndQrCodeUriAsync();
_isLoading = false;
StateHasChanged();
await JsInvokeService.RetryInvokeAsync("generateQrCode", TimeSpan.Zero, 5, "authenticator-uri");
}
}
private async Task OnValidSubmitAsync()
@@ -85,8 +96,13 @@ else
// Strip spaces and hyphens
var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty);
var is2FaTokenValid = await UserManager.VerifyTwoFactorTokenAsync(
UserService.User(), UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
var is2FaTokenValid = await UserManager.VerifyTwoFactorTokenAsync(user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode);
if (!is2FaTokenValid)
{
@@ -94,25 +110,31 @@ else
return;
}
await UserManager.SetTwoFactorEnabledAsync(UserService.User(), true);
await UserManager.SetTwoFactorEnabledAsync(user, true);
await AuthLoggingService.LogAuthEventSuccessAsync(UserService.User().UserName!, AuthEventType.TwoFactorAuthEnable);
Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", UserService.User().Id);
GlobalNotificationService.AddSuccessMessage("Your authenticator app has been verified.");
if (await UserManager.CountRecoveryCodesAsync(UserService.User()) == 0)
if (await UserManager.CountRecoveryCodesAsync(user) == 0)
{
RecoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(UserService.User(), 10);
RecoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
}
else
{
// Navigate back to the two factor authentication page.
// Navigate back to the two-factor authentication page.
NavigationService.RedirectTo("account/manage/2fa", forceLoad: true);
}
}
private async ValueTask LoadSharedKeyAndQrCodeUriAsync(AdminUser user)
private async ValueTask LoadSharedKeyAndQrCodeUriAsync()
{
// Load the authenticator key & QR code URI to display on the form
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
// Load the authenticator key & QR code URI to display on the form.
var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
if (string.IsNullOrEmpty(unformattedKey))
{
@@ -126,7 +148,7 @@ else
AuthenticatorUri = GenerateQrCodeUri(username!, unformattedKey!);
}
private string FormatKey(string unformattedKey)
private static string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;

View File

@@ -7,9 +7,9 @@
<LayoutPageTitle>Generate two-factor authentication (2FA) recovery codes</LayoutPageTitle>
@if (recoveryCodes is not null)
@if (_recoveryCodes is not null)
{
<ShowRecoveryCodes RecoveryCodes="recoveryCodes.ToArray()"/>
<ShowRecoveryCodes RecoveryCodes="_recoveryCodes.ToArray()"/>
}
else
{
@@ -35,14 +35,20 @@ else
}
@code {
private IEnumerable<string>? recoveryCodes;
private IEnumerable<string>? _recoveryCodes;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(UserService.User());
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
if (!isTwoFactorEnabled)
{
throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled.");
@@ -51,11 +57,16 @@ else
private async Task GenerateCodes()
{
var userId = await UserManager.GetUserIdAsync(UserService.User());
recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(UserService.User(), 10);
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
_recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10);
GlobalNotificationService.AddSuccessMessage("You have generated new recovery codes.");
Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId);
Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", UserService.User().Id);
}
}

View File

@@ -30,15 +30,19 @@
@code {
private async Task OnSubmitAsync()
{
await UserManager.SetTwoFactorEnabledAsync(UserService.User(), false);
await UserManager.ResetAuthenticatorKeyAsync(UserService.User());
var userId = await UserManager.GetUserIdAsync(UserService.User());
Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId);
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
await UserManager.SetTwoFactorEnabledAsync(user, false);
await UserManager.ResetAuthenticatorKeyAsync(user);
Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", UserService.User().Id);
GlobalNotificationService.AddSuccessMessage("Your authenticator app key has been reset, you will need to re-configure your authenticator app using the new key.");
NavigationService.RedirectTo(
"account/manage/2fa");
NavigationService.RedirectTo("account/manage/2fa");
}
}

View File

@@ -5,29 +5,29 @@
<LayoutPageTitle>Two-factor authentication (2FA)</LayoutPageTitle>
@if (is2FaEnabled)
@if (_is2FaEnabled)
{
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Two-factor authentication (2FA)</h3>
@if (recoveryCodesLeft == 0)
@if (_recoveryCodesLeft == 0)
{
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 dark:bg-red-900 dark:text-red-100">
<p class="font-bold">You have no recovery codes left.</p>
<p>You must <a href="account/manage/generate-recovery-codes" class="text-red-800 dark:text-red-200 underline">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if (recoveryCodesLeft == 1)
else if (_recoveryCodesLeft == 1)
{
<div class="mb-4 p-4 bg-red-100 border-l-4 border-red-500 text-red-700 dark:bg-red-900 dark:text-red-100">
<p class="font-bold">You have 1 recovery code left.</p>
<p>You can <a href="account/manage/generate-recovery-codes" class="text-red-800 dark:text-red-200 underline">generate a new set of recovery codes</a>.</p>
</div>
}
else if (recoveryCodesLeft <= 3)
else if (_recoveryCodesLeft <= 3)
{
<div class="mb-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700 dark:bg-yellow-900 dark:text-yellow-100">
<p class="font-bold">You have @recoveryCodesLeft recovery codes left.</p>
<p class="font-bold">You have @_recoveryCodesLeft recovery codes left.</p>
<p>You should <a href="account/manage/generate-recovery-codes" class="text-yellow-800 dark:text-yellow-200 underline">generate a new set of recovery codes</a>.</p>
</div>
}
@@ -42,7 +42,7 @@
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h4 class="text-lg font-semibold text-gray-900 dark:text-white mb-4">Authenticator app</h4>
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2">
@if (!hasAuthenticator)
@if (!_hasAuthenticator)
{
<LinkButton Href="account/manage/enable-authenticator" Color="primary" Text="Add authenticator app" />
}
@@ -55,17 +55,23 @@
</div>
@code {
private bool hasAuthenticator;
private int recoveryCodesLeft;
private bool is2FaEnabled;
private bool _hasAuthenticator;
private int _recoveryCodesLeft;
private bool _is2FaEnabled;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(UserService.User()) is not null;
is2FaEnabled = await UserManager.GetTwoFactorEnabledAsync(UserService.User());
recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(UserService.User());
var user = await UserManager.FindByIdAsync(UserService.User().Id);
if (user == null)
{
throw new InvalidOperationException("User not found.");
}
_hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null;
_is2FaEnabled = await UserManager.GetTwoFactorEnabledAsync(user);
_recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user);
}
}

View File

@@ -105,11 +105,13 @@
// Get unique users who either:
// 1. Have successful auth logs
// 2. Have updated their vault
var activeUsers = await DbContext.AuthLogs
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var activeUsers = await dbContext.AuthLogs
.Where(l => l.Timestamp >= since && l.IsSuccess)
.Select(l => l.Username)
.Union(
DbContext.Vaults
dbContext.Vaults
.Where(v => v.UpdatedAt >= since)
.Select(v => v.User.UserName!)
)

View File

@@ -43,7 +43,8 @@
var last14Days = now.AddDays(-14);
// Get email statistics
var emailQuery = DbContext.Emails.AsQueryable();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var emailQuery = dbContext.Emails.AsQueryable();
EmailStats = new EmailStatistics
{
Last24Hours = await emailQuery.CountAsync(e => e.DateSystem >= last24Hours),

View File

@@ -43,7 +43,8 @@
var last14Days = now.AddDays(-14);
// Get registration statistics
var registrationQuery = DbContext.AliasVaultUsers.AsQueryable();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var registrationQuery = dbContext.AliasVaultUsers.AsQueryable();
RegistrationStats = new RegistrationStatistics
{
Last24Hours = await registrationQuery.CountAsync(u => u.CreatedAt >= last24Hours),

View File

@@ -90,7 +90,8 @@ else
IsLoading = true;
StateHasChanged();
IQueryable<Email> query = DbContext.Emails;
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
IQueryable<Email> query = dbContext.Emails;
// Apply sort
switch (SortColumn)

View File

@@ -3,21 +3,21 @@
<LayoutPageTitle>Error</LayoutPageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h1 class="text-danger dark:text-red-400">Error.</h1>
<h2 class="text-danger dark:text-red-400">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
<strong>Request ID:</strong> <code class="dark:bg-gray-700 dark:text-gray-200">@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
<h3 class="dark:text-white">Development Mode</h3>
<p class="dark:text-gray-300">
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<p class="dark:text-gray-300">
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>

View File

@@ -49,7 +49,7 @@ else
<SortableTableColumn>@log.Timestamp.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@log.Username</SortableTableColumn>
<SortableTableColumn>@log.EventType</SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="Failed" /></SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="log.IsSuccess" TextTrue="Success" TextFalse="@log.FailureReason.ToString()" /></SortableTableColumn>
<SortableTableColumn>@log.IpAddress</SortableTableColumn>
</SortableTableRow>
}
@@ -128,10 +128,8 @@ else
private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
var query = DbContext.AuthLogs.AsQueryable();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.AuthLogs.AsQueryable();
if (!string.IsNullOrEmpty(SearchTerm))
{
@@ -215,8 +213,9 @@ else
IsLoading = true;
StateHasChanged();
DbContext.AuthLogs.RemoveRange(DbContext.AuthLogs);
await DbContext.SaveChangesAsync();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.AuthLogs.RemoveRange(dbContext.AuthLogs);
await dbContext.SaveChangesAsync();
await RefreshData();
IsLoading = false;

View File

@@ -135,7 +135,8 @@ else
{
if (firstRender)
{
ServiceNames = await DbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
ServiceNames = await dbContext.Logs.Select(l => l.Application).Distinct().ToListAsync();
await RefreshData();
}
}
@@ -148,10 +149,8 @@ else
private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
var query = DbContext.Logs.AsQueryable();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.Logs.AsQueryable();
if (!string.IsNullOrEmpty(SearchTerm))
{
@@ -218,8 +217,9 @@ else
IsLoading = true;
StateHasChanged();
DbContext.Logs.RemoveRange(DbContext.Logs);
await DbContext.SaveChangesAsync();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.Logs.RemoveRange(dbContext.Logs);
await dbContext.SaveChangesAsync();
await RefreshData();
IsLoading = false;

View File

@@ -50,16 +50,10 @@ public abstract class MainBase : OwningComponentBase
protected JsInvokeService JsInvokeService { get; set; } = null!;
/// <summary>
/// Gets or sets the AliasServerDbContext instance.
/// Gets or sets the IAliasServerDbContextFactory instance.
/// </summary>
[Inject]
protected AliasServerDbContext DbContext { get; set; } = null!;
/// <summary>
/// Gets or sets the AliasServerDbContextFactory instance.
/// </summary>
[Inject]
protected IDbContextFactory<AliasServerDbContext> DbContextFactory { get; set; } = null!;
protected IAliasServerDbContextFactory DbContextFactory { get; set; } = null!;
/// <summary>
/// Gets or sets the GlobalLoadingService in order to manipulate the global loading spinner animation.

View File

@@ -0,0 +1,102 @@
@using AliasVault.RazorComponents.Tables
@using AliasVault.Shared.Models.Enums
@inherits MainBase
<div class="mb-4">
<Paginator CurrentPage="CurrentPage" PageSize="PageSize" TotalRecords="TotalRecords" OnPageChanged="HandlePageChanged" />
<SortableTable Columns="@_tableColumns" SortColumn="@SortColumn" SortDirection="@SortDirection" OnSortChanged="HandleSortChanged">
@foreach (var job in JobList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@job.Id</SortableTableColumn>
<SortableTableColumn>@job.RunDate.ToString("yyyy-MM-dd")</SortableTableColumn>
<SortableTableColumn>@job.StartTime.ToString("HH:mm")</SortableTableColumn>
<SortableTableColumn>@(job.EndTime?.ToString("HH:mm") ?? "-")</SortableTableColumn>
<SortableTableColumn>
@{
string bgColor = job.Status switch
{
TaskRunnerJobStatus.Pending => "bg-yellow-500",
TaskRunnerJobStatus.Running => "bg-blue-500",
TaskRunnerJobStatus.Finished => "bg-green-500",
TaskRunnerJobStatus.Error => "bg-red-500",
_ => "bg-gray-500"
};
}
<span class="px-2 py-1 rounded-full text-white @bgColor">
@job.Status
</span>
</SortableTableColumn>
<SortableTableColumn>@(job.IsOnDemand ? "Yes" : "No")</SortableTableColumn>
<SortableTableColumn Title="@job.ErrorMessage">
@if (!string.IsNullOrEmpty(job.ErrorMessage))
{
<span class="text-red-600 dark:text-red-400">@(job.ErrorMessage.Length > 50 ? job.ErrorMessage[..50] + "..." : job.ErrorMessage)</span>
}
</SortableTableColumn>
</SortableTableRow>
}
</SortableTable>
</div>
@code {
private readonly List<TableColumn> _tableColumns =
[
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Date", PropertyName = "RunDate" },
new TableColumn { Title = "Start", PropertyName = "StartTime" },
new TableColumn { Title = "End", PropertyName = "EndTime" },
new TableColumn { Title = "Status", PropertyName = "Status" },
new TableColumn { Title = "On-Demand", PropertyName = "IsOnDemand" },
new TableColumn { Title = "Error", PropertyName = "ErrorMessage" },
];
private List<TaskRunnerJob> JobList { get; set; } = [];
private int CurrentPage { get; set; } = 1;
private int PageSize { get; set; } = 5;
private int TotalRecords { get; set; }
private string SortColumn { get; set; } = "Id";
private SortDirection SortDirection { get; set; } = SortDirection.Descending;
/// <summary>
/// Refreshes the data displayed in the table.
/// </summary>
public async Task RefreshData()
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var query = dbContext.TaskRunnerJobs.AsQueryable();
// Apply sorting
query = SortDirection == SortDirection.Ascending
? query.OrderBy(x => EF.Property<object>(x, SortColumn))
: query.OrderByDescending(x => EF.Property<object>(x, SortColumn));
TotalRecords = await query.CountAsync();
JobList = await query
.Skip((CurrentPage - 1) * PageSize)
.Take(PageSize)
.ToListAsync();
StateHasChanged();
}
/// <inheritdoc/>
protected override async Task OnInitializedAsync()
{
await RefreshData();
}
private async Task HandlePageChanged(int newPage)
{
CurrentPage = newPage;
await RefreshData();
}
private async Task HandleSortChanged((string column, SortDirection direction) sort)
{
SortColumn = sort.column;
SortDirection = sort.direction;
await RefreshData();
}
}

View File

@@ -1,7 +1,10 @@
@page "/settings/server"
@inject ServerSettingsService SettingsService
@inject ILogger<ServerSettingsService> Logger
@using AliasVault.Shared.Models.Enums
@using AliasVault.Shared.Server.Models
@using AliasVault.Shared.Server.Services
@using AliasVault.Admin.Main.Pages.Settings.Components
@inherits MainBase
<LayoutPageTitle>Server settings</LayoutPageTitle>
@@ -11,11 +14,28 @@
Title="Server settings"
Description="Configure AliasVault server settings.">
<CustomActions>
<RefreshButton OnClick="RefreshData" ButtonText="Refresh" />
<ConfirmButton OnClick="SaveSettings">Save changes</ConfirmButton>
</CustomActions>
</PageHeader>
<div class="px-4">
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Authentication Settings</h3>
<div class="grid gap-4 mb-4 sm:grid-cols-2 sm:gap-6 sm:mb-5">
<div>
<label for="refreshTokenShort" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Short Refresh Token Lifetime (hours)</label>
<input type="number" @bind="Settings.RefreshTokenLifetimeShort" id="refreshTokenShort" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Determines how long the user stays logged in after inactivity. Used when "Remember me" is not checked during login.</p>
</div>
<div>
<label for="refreshTokenLong" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Long Refresh Token Lifetime (hours)</label>
<input type="number" @bind="Settings.RefreshTokenLifetimeLong" id="refreshTokenLong" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500">
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Determines how long the user stays logged in after inactivity. Used when "Remember me" is checked during login.</p>
</div>
</div>
</div>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Data Retention</h3>
<div class="grid gap-4 mb-4 sm:grid-cols-2 sm:gap-6 sm:mb-5">
@@ -40,7 +60,9 @@
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Set to 0 for unlimited emails</p>
</div>
</div>
</div>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Maintenance Schedule</h3>
<div class="mb-4">
<label for="schedule" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Time (24h format)</label>
@@ -60,12 +82,23 @@
}
</div>
</div>
<div class="mb-4">
<h4 class="mb-2 text-md font-medium text-gray-900 dark:text-white">Manual Execution</h4>
<ConfirmButton OnClick="RunMaintenanceTasksNow">Run Maintenance Tasks Now</ConfirmButton>
</div>
</div>
<div class="p-4 mb-4 mx-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 sm:p-6 dark:bg-gray-800">
<h3 class="mb-4 text-lg font-medium text-gray-900 dark:text-white">Maintenance History</h3>
<TaskRunnerHistory @ref="_taskRunnerHistoryComponent" />
</div>
</div>
@code {
private ServerSettingsModel Settings { get; set; } = new();
private TaskRunnerHistory? _taskRunnerHistoryComponent;
private ServerSettingsModel Settings { get; set; } = new();
private readonly Dictionary<int, string> DaysOfWeek = new()
{
{ 1, "Monday" },
@@ -86,9 +119,13 @@
private void ToggleDay(int day)
{
if (Settings.TaskRunnerDays.Contains(day))
{
Settings.TaskRunnerDays.Remove(day);
}
else
{
Settings.TaskRunnerDays.Add(day);
}
}
private async Task SaveSettings()
@@ -96,4 +133,50 @@
await SettingsService.SaveSettingsAsync(Settings);
GlobalNotificationService.AddSuccessMessage("Settings saved successfully", true);
}
private async Task RunMaintenanceTasksNow()
{
try
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var job = new TaskRunnerJob
{
Name = nameof(TaskRunnerJobType.Maintenance),
RunDate = DateTime.UtcNow.Date,
StartTime = TimeOnly.FromDateTime(DateTime.UtcNow),
Status = TaskRunnerJobStatus.Pending,
IsOnDemand = true
};
dbContext.TaskRunnerJobs.Add(job);
await dbContext.SaveChangesAsync();
// Refresh the history component to show the new job
if (_taskRunnerHistoryComponent != null)
{
await _taskRunnerHistoryComponent.RefreshData();
}
Logger.LogWarning("Maintenance tasks manually queued.");
GlobalNotificationService.AddSuccessMessage("Maintenance tasks queued. They will be executed on the next polling cycle (default every minute). Check the logs for details.", true);
}
catch (Exception ex)
{
GlobalNotificationService.AddErrorMessage($"Failed to start maintenance tasks: {ex.Message}", true);
}
}
/// <summary>
/// Refreshes the data displayed on the page.
/// </summary>
private async Task RefreshData()
{
Settings = await SettingsService.GetAllSettingsAsync();
// Refresh the history component to show the new job
if (_taskRunnerHistoryComponent != null)
{
await _taskRunnerHistoryComponent.RefreshData();
}
}
}

View File

@@ -60,7 +60,8 @@ else
if (firstRender)
{
// Load existing Obj.
Obj = await DbContext.AliasVaultUsers.FindAsync(Id);
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
Obj = await dbContext.AliasVaultUsers.FindAsync(Id);
// Hide loading spinner
IsLoading = false;
@@ -83,8 +84,9 @@ else
// Add log entry.
Logger.LogWarning("Deleted user {UserName} ({UserId}).", Obj.UserName, Obj.Id);
DbContext.AliasVaultUsers.Remove(Obj);
await DbContext.SaveChangesAsync();
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
dbContext.AliasVaultUsers.Remove(Obj);
await dbContext.SaveChangesAsync();
GlobalNotificationService.AddSuccessMessage("User successfully deleted.");
GlobalLoadingSpinner.Hide();

View File

@@ -30,14 +30,19 @@ else
@foreach (var user in UserList)
{
<SortableTableRow>
<SortableTableColumn IsPrimary="true">@user.Id</SortableTableColumn>
<SortableTableColumn>@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn IsPrimary="true">@user.CreatedAt.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>@user.UserName</SortableTableColumn>
<SortableTableColumn>@user.VaultCount</SortableTableColumn>
<SortableTableColumn>@user.EmailClaimCount</SortableTableColumn>
<SortableTableColumn>@Math.Round((double)user.VaultStorageInKb / 1024, 1) MB</SortableTableColumn>
<SortableTableColumn><StatusPill Enabled="user.TwoFactorEnabled" /></SortableTableColumn>
<SortableTableColumn>@user.LastVaultUpdate.ToString("yyyy-MM-dd HH:mm")</SortableTableColumn>
<SortableTableColumn>
@if (user.Blocked)
{
<StatusPill Enabled="false" TextFalse="Blocked" />
}
</SortableTableColumn>
<SortableTableColumn>
<LinkButton Color="primary" Href="@($"users/{user.Id}")" Text="View" />
</SortableTableColumn>
@@ -49,7 +54,6 @@ else
@code {
private readonly List<TableColumn> _tableColumns = [
new TableColumn { Title = "ID", PropertyName = "Id" },
new TableColumn { Title = "Registered", PropertyName = "CreatedAt" },
new TableColumn { Title = "Username", PropertyName = "UserName" },
new TableColumn { Title = "# Vaults", PropertyName = "VaultCount" },
@@ -57,6 +61,7 @@ else
new TableColumn { Title = "Storage", PropertyName = "VaultStorageInKb" },
new TableColumn { Title = "2FA", PropertyName = "TwoFactorEnabled" },
new TableColumn { Title = "LastVaultUpdate", PropertyName = "LastVaultUpdate" },
new TableColumn { Title = "Status", Sortable = false },
new TableColumn { Title = "Actions", Sortable = false},
];
@@ -107,10 +112,8 @@ else
private async Task RefreshData()
{
IsLoading = true;
StateHasChanged();
IQueryable<AliasVaultUser> query = DbContext.AliasVaultUsers;
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
IQueryable<AliasVaultUser> query = dbContext.AliasVaultUsers;
if (SearchTerm.Length > 0)
{
@@ -130,6 +133,7 @@ else
u.UserName,
u.CreatedAt,
u.TwoFactorEnabled,
u.Blocked,
Vaults = u.Vaults.Select(v => new
{
v.FileSize,
@@ -147,6 +151,7 @@ else
Id = user.Id,
UserName = user.UserName?.ToLower() ?? "N/A",
TwoFactorEnabled = user.TwoFactorEnabled,
Blocked = user.Blocked,
CreatedAt = user.CreatedAt,
VaultCount = user.Vaults.Count(),
EmailClaimCount = user.EmailClaims.Count(),

View File

@@ -48,6 +48,17 @@ else
}
}
</div>
<div class="flex items-center space-x-2 mt-4">
<span class="text-sm font-medium text-gray-900 dark:text-white">Account Status:</span>
<StatusPill Enabled="@(!User.Blocked)" TextTrue="Active" TextFalse="Blocked" />
<Button Color="@(User.Blocked ? "success" : "danger")" OnClick="ToggleBlockStatus">
@(User.Blocked ? "Unblock User" : "Block User")
</Button>
<span class="text-sm text-gray-500 dark:text-gray-400">
<i class="fas fa-info-circle mr-1"></i>
Blocking a user prevents them from logging in or accessing AliasVault
</span>
</div>
</div>
</div>
</div>
@@ -124,10 +135,11 @@ else
StateHasChanged();
// Load the aliases from the webapi via AliasService.
User = await DbContext.AliasVaultUsers.FindAsync(Id);
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
User = await dbContext.AliasVaultUsers.FindAsync(Id);
// Get count of user authenticator tokens.
TwoFactorKeysCount = await DbContext.UserTokens.CountAsync(x => x.UserId == User!.Id && x.Name == "AuthenticatorKey");
TwoFactorKeysCount = await dbContext.UserTokens.CountAsync(x => x.UserId == User!.Id && x.Name == "AuthenticatorKey");
if (User is null)
{
@@ -138,7 +150,7 @@ else
}
// Load all active refresh tokens for this user to show which devices are logged in.
RefreshTokenList = await DbContext.AliasVaultUserRefreshTokens.Where(x => x.UserId == User.Id).Select(x => new AliasVaultUserRefreshToken()
RefreshTokenList = await dbContext.AliasVaultUserRefreshTokens.Where(x => x.UserId == User.Id).Select(x => new AliasVaultUserRefreshToken()
{
Id = x.Id,
DeviceIdentifier = x.DeviceIdentifier,
@@ -151,7 +163,7 @@ else
.ToListAsync();
// Load all vaults for this user (do not load the actual file content for performance reasons).
VaultList = await DbContext.Vaults.Where(x => x.UserId == User.Id).Select(x => new Vault
VaultList = await dbContext.Vaults.Where(x => x.UserId == User.Id).Select(x => new Vault
{
Id = x.Id,
Version = x.Version,
@@ -171,7 +183,7 @@ else
.ToListAsync();
// Load all email claims for this user.
EmailClaimList = await DbContext.UserEmailClaims
EmailClaimList = await dbContext.UserEmailClaims
.Where(x => x.UserId == User.Id)
.Select(x => new UserEmailClaimWithCount
{
@@ -181,7 +193,7 @@ else
AddressDomain = x.AddressDomain,
CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt,
EmailCount = DbContext.Emails.Count(e => e.To == x.Address)
EmailCount = dbContext.Emails.Count(e => e.To == x.Address)
})
.OrderBy(x => x.CreatedAt)
.ToListAsync();
@@ -195,12 +207,13 @@ else
/// </summary>
private async Task RevokeRefreshToken(AliasVaultUserRefreshToken entry)
{
var token = await DbContext.AliasVaultUserRefreshTokens.FindAsync(entry.Id);
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
var token = await dbContext.AliasVaultUserRefreshTokens.FindAsync(entry.Id);
if (token != null)
{
DbContext.AliasVaultUserRefreshTokens.Remove(token);
await DbContext.SaveChangesAsync();
dbContext.AliasVaultUserRefreshTokens.Remove(token);
await dbContext.SaveChangesAsync();
await RefreshData();
}
}
@@ -211,12 +224,13 @@ else
/// </summary>
private async Task EnableTwoFactor()
{
User = await DbContext.AliasVaultUsers.FindAsync(Id);
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
User = await dbContext.AliasVaultUsers.FindAsync(Id);
if (User != null)
{
User.TwoFactorEnabled = true;
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
await RefreshData();
}
}
@@ -228,12 +242,13 @@ else
/// </summary>
private async Task DisableTwoFactor()
{
User = await DbContext.AliasVaultUsers.FindAsync(Id);
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
User = await dbContext.AliasVaultUsers.FindAsync(Id);
if (User != null)
{
User.TwoFactorEnabled = false;
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
await RefreshData();
}
}
@@ -245,16 +260,17 @@ else
/// </summary>
private async Task ResetTwoFactor()
{
User = await DbContext.AliasVaultUsers.FindAsync(Id);
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
User = await dbContext.AliasVaultUsers.FindAsync(Id);
if (User != null)
{
// Remove all authenticator keys and recovery codes.
await DbContext.UserTokens
await dbContext.UserTokens
.Where(x => x.UserId == User.Id && (x.Name == "AuthenticatorKey" || x.Name == "RecoveryCodes"))
.ForEachAsync(x => DbContext.UserTokens.Remove(x));
.ForEachAsync(x => dbContext.UserTokens.Remove(x));
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
await RefreshData();
}
}
@@ -265,6 +281,7 @@ else
/// <param name="vault">The vault to make current.</param>
private async Task MakeCurrentAsync(Vault vault)
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
if (await ConfirmModalService.ShowConfirmation(
title: "Confirm Vault Restoration",
message: @"Are you sure you want to restore this specific vault and make it the active one?
@@ -275,7 +292,7 @@ Important notes:
Do you want to proceed with the restoration?")) {
// Load vault
var currentVault = await DbContext.Vaults.FindAsync(vault.Id);
var currentVault = await dbContext.Vaults.FindAsync(vault.Id);
if (currentVault == null)
{
return;
@@ -285,10 +302,26 @@ Do you want to proceed with the restoration?")) {
currentVault.RevisionNumber = VaultList.MaxBy(x => x.RevisionNumber)!.RevisionNumber + 1;
// Save it.
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync();
// Reload the page.
await RefreshData();
}
}
/// <summary>
/// Toggles the blocked status of the user.
/// </summary>
private async Task ToggleBlockStatus()
{
await using var dbContext = await DbContextFactory.CreateDbContextAsync();
User = await dbContext.AliasVaultUsers.FindAsync(Id);
if (User != null)
{
User.Blocked = !User.Blocked;
await dbContext.SaveChangesAsync();
await RefreshData();
}
}
}

View File

@@ -38,7 +38,7 @@ config.LastPasswordChanged = DateTime.Parse(lastPasswordChanged, CultureInfo.Inv
builder.Services.AddSingleton(config);
builder.Services.AddAliasVaultDataProtection("AliasVault.Api");
builder.Services.AddAliasVaultDataProtection("AliasVault.Admin");
// Add services to the container.
builder.Services.AddRazorComponents()
@@ -69,7 +69,7 @@ builder.Services.ConfigureApplicationCookie(options =>
options.LoginPath = "/user/login";
});
builder.Services.AddAliasVaultSqliteConfiguration();
builder.Services.AddAliasVaultDatabaseConfiguration(builder.Configuration);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddIdentityCore<AdminUser>(options =>
{
@@ -133,7 +133,7 @@ app.MapRazorComponents<App>()
using (var scope = app.Services.CreateScope())
{
var container = scope.ServiceProvider;
await using var db = await container.GetRequiredService<IDbContextFactory<AliasServerDbContext>>().CreateDbContextAsync();
await using var db = await container.GetRequiredService<IAliasServerDbContextFactory>().CreateDbContextAsync();
await db.Database.MigrateAsync();
await StartupTasks.CreateRolesIfNotExist(scope.ServiceProvider);

View File

@@ -15,34 +15,18 @@ using Microsoft.EntityFrameworkCore;
/// <summary>
/// User service for managing users.
/// </summary>
/// <param name="dbContext">AliasServerDbContext instance.</param>
/// <param name="dbContextFactory">AliasServerDbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
/// <param name="httpContextAccessor">HttpContextManager instance.</param>
public class UserService(AliasServerDbContext dbContext, UserManager<AdminUser> userManager, IHttpContextAccessor httpContextAccessor)
public class UserService(IAliasServerDbContextFactory dbContextFactory, UserManager<AdminUser> userManager, IHttpContextAccessor httpContextAccessor)
{
private const string AdminRole = "Admin";
private AdminUser? _user;
/// <summary>
/// The roles of the current user.
/// </summary>
private List<string> _userRoles = [];
/// <summary>
/// Whether the current user is an admin or not.
/// </summary>
private bool _isAdmin;
/// <summary>
/// Allow other components to subscribe to changes in the event object.
/// </summary>
public event Action OnChange = () => { };
/// <summary>
/// Gets a value indicating whether the User is loaded and available, false if not. Use this before accessing User() method.
/// </summary>
public bool UserLoaded => _user != null;
/// <summary>
/// Returns all users.
/// </summary>
@@ -84,15 +68,6 @@ public class UserService(AliasServerDbContext dbContext, UserManager<AdminUser>
return _user;
}
/// <summary>
/// Returns whether current user is admin or not.
/// </summary>
/// <returns>Boolean which indicates if user is admin.</returns>
public bool CurrentUserIsAdmin()
{
return _isAdmin;
}
/// <summary>
/// Returns current logged on user based on HttpContext.
/// </summary>
@@ -104,17 +79,11 @@ public class UserService(AliasServerDbContext dbContext, UserManager<AdminUser>
// Load user from database. Use a new context everytime to ensure we get the latest data.
var userName = httpContextAccessor.HttpContext?.User.Identity?.Name ?? string.Empty;
await using var dbContext = await dbContextFactory.CreateDbContextAsync();
var user = await dbContext.AdminUsers.FirstOrDefaultAsync(u => u.UserName == userName);
if (user != null)
{
_user = user;
// Load all roles for current user.
var roles = await userManager.GetRolesAsync(User());
_userRoles = roles.ToList();
// Define if current user is admin.
_isAdmin = _userRoles.Contains(AdminRole);
}
}
@@ -122,58 +91,6 @@ public class UserService(AliasServerDbContext dbContext, UserManager<AdminUser>
NotifyStateChanged();
}
/// <summary>
/// Returns current logged on user roles based on HttpContext.
/// </summary>
/// <returns>List of roles.</returns>
public async Task<List<string>> GetCurrentUserRolesAsync()
{
var roles = await userManager.GetRolesAsync(User());
return roles.ToList();
}
/// <summary>
/// Search for users based on search term.
/// </summary>
/// <param name="searchTerm">Search term.</param>
/// <returns>List of users matching the search term.</returns>
public async Task<List<AdminUser>> SearchUsersAsync(string searchTerm)
{
return await userManager.Users.Where(x => x.UserName != null && x.UserName.Contains(searchTerm)).Take(5).ToListAsync();
}
/// <summary>
/// Create a new user.
/// </summary>
/// <param name="user">User object.</param>
/// <param name="password">Password.</param>
/// <param name="roles">Roles.</param>
/// <returns>List of errors if there are any.</returns>
public async Task<List<string>> CreateUserAsync(AdminUser user, string password, List<string> roles)
{
var errors = await ValidateUser(user, password, isUpdate: false);
if (errors.Count > 0)
{
return errors;
}
var result = await userManager.CreateAsync(user, password);
if (!result.Succeeded)
{
foreach (var error in result.Errors)
{
errors.Add(error.Description);
}
return errors;
}
errors = await UpdateUserRolesAsync(user, roles);
return errors;
}
/// <summary>
/// Update user.
/// </summary>
@@ -228,48 +145,6 @@ public class UserService(AliasServerDbContext dbContext, UserManager<AdminUser>
return errors;
}
/// <summary>
/// Checks if supplied password is correct for the user.
/// </summary>
/// <param name="user">User object.</param>
/// <param name="password">The password to check.</param>
/// <returns>Boolean indicating whether supplied password is valid and matches what is stored in the database.</returns>
public async Task<bool> CheckPasswordAsync(AdminUser user, string password)
{
if (password.Length == 0)
{
return false;
}
return await userManager.CheckPasswordAsync(user, password);
}
/// <summary>
/// Update user roles. This is a separate method because it is called from both CreateUserAsync and UpdateUserAsync.
/// </summary>
/// <param name="user">User object.</param>
/// <param name="roles">New roles for the user.</param>
/// <returns>List of errors if any.</returns>
private async Task<List<string>> UpdateUserRolesAsync(AdminUser user, List<string> roles)
{
List<string> errors = new();
var currentRoles = await userManager.GetRolesAsync(user);
if (user.Id == User().Id && currentRoles.Contains(AdminRole) && !roles.Contains(AdminRole))
{
errors.Add("You cannot remove the Admin role from yourself if you are an Admin.");
return errors;
}
var rolesToAdd = roles.Except(currentRoles).ToList();
var rolesToRemove = currentRoles.Except(roles).ToList();
await userManager.AddToRolesAsync(user, rolesToAdd);
await userManager.RemoveFromRolesAsync(user, rolesToRemove);
return errors;
}
/// <summary>
/// Validate if user object contents conform to the requirements.
/// </summary>

View File

@@ -1,6 +1,7 @@
{
"DatabaseProvider": "postgresql",
"ConnectionStrings": {
"AliasServerDbContext": "Data Source=../../database/AliasServerDb.sqlite"
"AliasServerDbContext": "Host=localhost;Port=5433;Database=aliasvault;Username=aliasvault;Password=password"
},
"Logging": {
"LogLevel": {

View File

@@ -554,40 +554,6 @@ video {
--tw-contain-style: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.sr-only {
position: absolute;
width: 1px;
@@ -657,6 +623,11 @@ video {
margin-right: 0.75rem;
}
.mx-4 {
margin-left: 1rem;
margin-right: 1rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
@@ -1132,10 +1103,6 @@ video {
border-left-width: 4px;
}
.border-t {
border-top-width: 1px;
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@@ -1665,12 +1632,6 @@ video {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.shadow-sm {
--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
@@ -1750,6 +1711,11 @@ video {
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.hover\:text-gray-700:hover {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.hover\:text-gray-900:hover {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
@@ -1770,11 +1736,6 @@ video {
color: rgb(154 93 38 / var(--tw-text-opacity));
}
.hover\:text-gray-700:hover {
--tw-text-opacity: 1;
color: rgb(55 65 81 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
@@ -1905,6 +1866,10 @@ video {
background-color: rgb(30 58 138 / var(--tw-bg-opacity));
}
.dark\:bg-blue-900\/30:is(.dark *) {
background-color: rgb(30 58 138 / 0.3);
}
.dark\:bg-gray-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(75 85 99 / var(--tw-bg-opacity));
@@ -1915,6 +1880,10 @@ video {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:bg-gray-700\/50:is(.dark *) {
background-color: rgb(55 65 81 / 0.5);
}
.dark\:bg-gray-800:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@@ -1940,6 +1909,10 @@ video {
background-color: rgb(20 83 45 / var(--tw-bg-opacity));
}
.dark\:bg-green-900\/30:is(.dark *) {
background-color: rgb(20 83 45 / 0.3);
}
.dark\:bg-primary-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
@@ -1970,30 +1943,6 @@ video {
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-blue-900\/30:is(.dark *) {
background-color: rgb(30 58 138 / 0.3);
}
.dark\:bg-gray-700\/50:is(.dark *) {
background-color: rgb(55 65 81 / 0.5);
}
.dark\:bg-green-900\/30:is(.dark *) {
background-color: rgb(20 83 45 / 0.3);
}
.dark\:bg-blue-950\/80:is(.dark *) {
background-color: rgb(23 37 84 / 0.8);
}
.dark\:bg-gray-800\/80:is(.dark *) {
background-color: rgb(31 41 55 / 0.8);
}
.dark\:bg-green-950\/80:is(.dark *) {
background-color: rgb(5 46 22 / 0.8);
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -2117,6 +2066,11 @@ video {
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.dark\:hover\:text-gray-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.dark\:hover\:text-primary-500:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(244 149 65 / var(--tw-text-opacity));
@@ -2127,11 +2081,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:hover\:text-gray-300:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
}
.dark\:focus\:border-blue-500:focus:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
@@ -2147,6 +2096,11 @@ video {
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-blue-600:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity));
}
.dark\:focus\:ring-gray-600:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
@@ -2188,6 +2142,10 @@ video {
}
@media (min-width: 640px) {
.sm\:mb-5 {
margin-bottom: 1.25rem;
}
.sm\:flex {
display: flex;
}
@@ -2196,10 +2154,18 @@ video {
width: auto;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sm\:flex-row {
flex-direction: row;
}
.sm\:gap-6 {
gap: 1.5rem;
}
.sm\:space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));

View File

@@ -29,11 +29,12 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Databases\AliasServerDb\AliasServerDb.csproj" />
<ProjectReference Include="..\Shared\AliasVault.Shared.Server\AliasVault.Shared.Server.csproj" />
<ProjectReference Include="..\Shared\AliasVault.Shared\AliasVault.Shared.csproj" />
<ProjectReference Include="..\Utilities\AliasVault.Auth\AliasVault.Auth.csproj" />
<ProjectReference Include="..\Utilities\AliasVault.Logging\AliasVault.Logging.csproj" />

View File

@@ -0,0 +1,19 @@
//-----------------------------------------------------------------------
// <copyright file="Config.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;
/// <summary>
/// Configuration class for the Client project with values loaded from appsettings.json.
/// </summary>
public class Config
{
/// <summary>
/// Gets or sets a value indicating whether public registration is enabled.
/// </summary>
public bool PublicRegistrationEnabled { get; set; }
}

View File

@@ -20,6 +20,7 @@ using AliasVault.Shared.Models.WebApi;
using AliasVault.Shared.Models.WebApi.Auth;
using AliasVault.Shared.Models.WebApi.PasswordChange;
using AliasVault.Shared.Providers.Time;
using AliasVault.Shared.Server.Services;
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@@ -39,10 +40,12 @@ using SecureRemotePassword;
/// <param name="cache">IMemoryCache instance for persisting SRP values during multistep login process.</param>
/// <param name="timeProvider">ITimeProvider instance. This returns the time which can be mutated for testing.</param>
/// <param name="authLoggingService">AuthLoggingService instance. This is used to log auth attempts to the database.</param>
/// <param name="config">Config instance.</param>
/// <param name="settingsService">ServerSettingsService instance.</param>
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService) : ControllerBase
public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager, SignInManager<AliasVaultUser> signInManager, IConfiguration configuration, IMemoryCache cache, ITimeProvider timeProvider, AuthLoggingService authLoggingService, Config config, ServerSettingsService settingsService) : ControllerBase
{
/// <summary>
/// Error message for invalid username or password.
@@ -60,9 +63,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
private static readonly string[] InvalidRecoveryCode = ["Invalid recovery code."];
/// <summary>
/// Error message for invalid 2-factor authentication recovery code.
/// Error message for too many failed login attempts.
/// </summary>
private static readonly string[] AccountLocked = ["You have entered an incorrect password too many times and your account has now been locked out. You can try again in 30 minutes.."];
private static readonly string[] AccountLocked = ["You have entered an incorrect password too many times and your account has now been locked out. You can try again in 30 minutes."];
/// <summary>
/// Error message for if user is (manually) blocked by admin.
/// </summary>
private static readonly string[] AccountBlocked = ["Your account has been disabled. If you believe this is a mistake, please contact support."];
/// <summary>
/// Semaphore to prevent concurrent access to the database when generating new tokens for a user.
@@ -102,6 +110,13 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
return BadRequest(ServerValidationErrorResponse.Create(AccountLocked, 400));
}
// Check if the account is blocked.
if (user.Blocked)
{
await authLoggingService.LogAuthEventFailAsync(model.Username, AuthEventType.Login, AuthFailureReason.AccountBlocked);
return BadRequest(ServerValidationErrorResponse.Create(AccountBlocked, 400));
}
// Retrieve latest vault of user which contains the current salt and verifier.
var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user);
@@ -262,6 +277,13 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
return Unauthorized("User not found (name-2)");
}
// Check if the account is blocked.
if (user.Blocked)
{
await authLoggingService.LogAuthEventFailAsync(user.UserName!, AuthEventType.TokenRefresh, AuthFailureReason.AccountBlocked);
return Unauthorized("Account blocked");
}
// Generate new tokens for the user.
var token = await GenerateNewTokensForUser(user, tokenModel.RefreshToken);
if (token == null)
@@ -331,6 +353,12 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] RegisterRequest model)
{
// Check if public registration is disabled in the configuration.
if (!config.PublicRegistrationEnabled)
{
return BadRequest(ServerValidationErrorResponse.Create(["New account registration is currently disabled on this server. Please contact the administrator."], 400));
}
// Validate the username.
var (isValid, errorMessage) = ValidateUsername(model.Username);
if (!isValid)
@@ -603,6 +631,13 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
return (null, null, BadRequest(ServerValidationErrorResponse.Create(AccountLocked, 400)));
}
// Check if the account is blocked.
if (user.Blocked)
{
await authLoggingService.LogAuthEventFailAsync(model.Username, AuthEventType.Login, AuthFailureReason.AccountBlocked);
return (null, null, BadRequest(ServerValidationErrorResponse.Create(AccountBlocked, 400)));
}
// Validate the SRP session (actual password check).
var serverSession = AuthHelper.ValidateSrpSession(cache, user, model.ClientPublicEphemeral, model.ClientSessionProof);
if (serverSession is null)
@@ -655,18 +690,14 @@ public class AuthController(IDbContextFactory<AliasServerDbContext> dbContextFac
private async Task<TokenModel> GenerateNewTokensForUser(AliasVaultUser user, bool extendedLifetime = false)
{
await using var context = await dbContextFactory.CreateDbContextAsync();
var settings = await settingsService.GetAllSettingsAsync();
await Semaphore.WaitAsync();
try
{
// Determine the refresh token lifetime.
// - 4 hours by default.
// - 7 days if "remember me" was checked during login.
var refreshTokenLifetime = TimeSpan.FromHours(4);
if (extendedLifetime)
{
refreshTokenLifetime = TimeSpan.FromDays(7);
}
// Use server settings for refresh token lifetime.
var refreshTokenLifetimeHours = extendedLifetime ? settings.RefreshTokenLifetimeLong : settings.RefreshTokenLifetimeShort;
var refreshTokenLifetime = TimeSpan.FromHours(refreshTokenLifetimeHours);
// Return new refresh token.
return await GenerateRefreshToken(user, refreshTokenLifetime);

View File

@@ -23,7 +23,7 @@ using Microsoft.EntityFrameworkCore;
/// <param name="dbContextFactory">DbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
[ApiVersion("1")]
public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
public class EmailBoxController(IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Returns a list of emails for the provided email address.
@@ -41,9 +41,11 @@ public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContex
return Unauthorized("Not authenticated.");
}
var sanitizedEmail = to.Trim().ToLower();
// See if this user has a valid claim to the email address.
var emailClaim = await context.UserEmailClaims
.FirstOrDefaultAsync(x => x.Address == to);
.FirstOrDefaultAsync(x => x.Address == sanitizedEmail);
if (emailClaim is null)
{
@@ -51,7 +53,7 @@ public class EmailBoxController(IDbContextFactory<AliasServerDbContext> dbContex
{
Message = "No claim exists for this email address.",
Code = "CLAIM_DOES_NOT_EXIST",
Details = new { ProvidedEmail = to },
Details = new { ProvidedEmail = sanitizedEmail },
StatusCode = StatusCodes.Status400BadRequest,
Timestamp = DateTime.UtcNow,
});

View File

@@ -22,7 +22,7 @@ using Microsoft.EntityFrameworkCore;
/// <param name="dbContextFactory">DbContext instance.</param>
/// <param name="userManager">UserManager instance.</param>
[ApiVersion("1")]
public class EmailController(ILogger<VaultController> logger, IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
public class EmailController(ILogger<VaultController> logger, IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Get the newest version of the vault for the current user.

View File

@@ -16,7 +16,7 @@ using Microsoft.EntityFrameworkCore;
/// </summary>
[ApiController]
[Route("/")]
public class RootController(IDbContextFactory<AliasServerDbContext> dbContextFactory) : ControllerBase
public class RootController(IAliasServerDbContextFactory dbContextFactory) : ControllerBase
{
/// <summary>
/// Root endpoint that returns a 200 OK if the database connection is successful

View File

@@ -24,7 +24,7 @@ using Microsoft.EntityFrameworkCore;
[Route("v{version:apiVersion}/[controller]")]
[ApiController]
[ApiVersion("1")]
public class SecurityController(IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
public class SecurityController(IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Returns list of active sessions (refresh tokens) for the current user.

View File

@@ -36,7 +36,7 @@ using Microsoft.Extensions.Caching.Memory;
/// <param name="authLoggingService">AuthLoggingService instance.</param>
/// <param name="cache">IMemoryCache instance.</param>
[ApiVersion("1")]
public class VaultController(ILogger<VaultController> logger, IDbContextFactory<AliasServerDbContext> dbContextFactory, UserManager<AliasVaultUser> userManager, ITimeProvider timeProvider, AuthLoggingService authLoggingService, IMemoryCache cache) : AuthenticatedRequestController(userManager)
public class VaultController(ILogger<VaultController> logger, IAliasServerDbContextFactory dbContextFactory, UserManager<AliasVaultUser> userManager, ITimeProvider timeProvider, AuthLoggingService authLoggingService, IMemoryCache cache) : AuthenticatedRequestController(userManager)
{
/// <summary>
/// Error message for providing an invalid current password (during password change).
@@ -89,6 +89,7 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
Status = VaultStatus.Ok,
Vault = new Shared.Models.WebApi.Vault.Vault
{
Username = user.UserName!,
Blob = string.Empty,
Version = string.Empty,
CurrentRevisionNumber = 0,
@@ -121,6 +122,7 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
Status = VaultStatus.Ok,
Vault = new Shared.Models.WebApi.Vault.Vault
{
Username = user.UserName!,
Blob = vault.VaultBlob,
Version = vault.Version,
CurrentRevisionNumber = vault.RevisionNumber,
@@ -159,6 +161,7 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
{
Vaults = vaultsToMerge.Select(x => new Shared.Models.WebApi.Vault.Vault
{
Username = user.UserName!,
Blob = x.VaultBlob,
Version = x.Version,
CurrentRevisionNumber = x.RevisionNumber,
@@ -187,6 +190,15 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
return Unauthorized();
}
// Compare the logged-in username with the username in the provided vault model.
// If they do not match reject the request. This is important because it's
// possible that a user has logged in with a different username than the one
// that is being used to update the vault (e.g. if working with multiple tabs).
if (user.UserName != model.Username)
{
return BadRequest("The currently logged on user is not the owner of the vault being saved. Please save your changes locally and log out and in again.");
}
// Retrieve latest vault of user which contains the current encryption settings.
var latestVault = user.Vaults.OrderByDescending(x => x.RevisionNumber).Select(x => new { x.Salt, x.Verifier, x.EncryptionType, x.EncryptionSettings, x.RevisionNumber }).First();
@@ -256,6 +268,15 @@ public class VaultController(ILogger<VaultController> logger, IDbContextFactory<
return Unauthorized();
}
// Compare the logged-in username with the username in the provided vault model.
// If they do not match reject the request. This is important because it's
// possible that a user has logged in with a different username than the one
// that is being used to update the vault (e.g. if working with multiple tabs).
if (model.Username != user.UserName)
{
return BadRequest("The currently logged on user is not the owner of the vault being saved. Please save your changes locally and log out and in again.");
}
// Validate the SRP session (actual password check).
var serverSession = AuthHelper.ValidateSrpSession(cache, user, model.CurrentClientPublicEphemeral, model.CurrentClientSessionProof);
if (serverSession is null)

View File

@@ -9,11 +9,13 @@ using System.Reflection;
using System.Text;
using AliasServerDb;
using AliasServerDb.Configuration;
using AliasVault.Api;
using AliasVault.Api.Jwt;
using AliasVault.Auth;
using AliasVault.Cryptography.Server;
using AliasVault.Logging;
using AliasVault.Shared.Providers.Time;
using AliasVault.Shared.Server.Services;
using Asp.Versioning;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
@@ -24,12 +26,20 @@ using Microsoft.OpenApi.Models;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
builder.Configuration.AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true, reloadOnChange: true);
var config = new Config();
var publicRegistrationEnabled = Environment.GetEnvironmentVariable("PUBLIC_REGISTRATION_ENABLED") ?? "false";
config.PublicRegistrationEnabled = bool.Parse(publicRegistrationEnabled);
builder.Services.AddSingleton(config);
builder.Services.ConfigureLogging(builder.Configuration, Assembly.GetExecutingAssembly().GetName().Name!, "../../logs");
builder.Services.AddAliasVaultDataProtection("AliasVault.Api");
builder.Services.AddSingleton<ITimeProvider, SystemTimeProvider>();
builder.Services.AddScoped<TimeValidationJwtBearerEvents>();
builder.Services.AddScoped<AuthLoggingService>();
builder.Services.AddScoped<ServerSettingsService>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddLogging(logging =>
@@ -40,7 +50,7 @@ builder.Services.AddLogging(logging =>
logging.AddFilter("Microsoft.AspNetCore.Identity.UserManager", LogLevel.Error);
});
builder.Services.AddAliasVaultSqliteConfiguration();
builder.Services.AddAliasVaultDatabaseConfiguration(builder.Configuration);
builder.Services.AddIdentity<AliasVaultUser, AliasVaultRole>(options =>
{
options.Password.RequireDigit = false;
@@ -170,7 +180,7 @@ app.MapControllers();
using (var scope = app.Services.CreateScope())
{
var container = scope.ServiceProvider;
await using var db = await container.GetRequiredService<IDbContextFactory<AliasServerDbContext>>().CreateDbContextAsync();
await using var db = await container.GetRequiredService<IAliasServerDbContextFactory>().CreateDbContextAsync();
await db.Database.MigrateAsync();
}

View File

@@ -7,7 +7,8 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development"
"DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true"
},
"dotnetRunMessages": true,
"applicationUrl": "http://0.0.0.0:5092"
@@ -19,7 +20,8 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development"
"DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true"
},
"dotnetRunMessages": true,
"applicationUrl": "https://0.0.0.0:7223"

View File

@@ -11,8 +11,9 @@
"Jwt": {
"Issuer": "AliasVault"
},
"DatabaseProvider": "postgresql",
"ConnectionStrings": {
"AliasServerDbContext": "Data Source=../../database/AliasServerDb.sqlite"
"AliasServerDbContext": "Host=localhost;Port=5433;Database=aliasvault;Username=aliasvault;Password=password"
},
"AllowedHosts": "*"
}

View File

@@ -1,9 +1,9 @@
<a href="/">
<div class="text-5xl font-bold text-gray-900 dark:text-white mb-4 flex items-center">
<img src="img/logo.svg" alt="AliasVault" class="w-20 h-20 mr-2" />
<span class="relative">
<span class="relative inline-flex flex-wrap items-center">
AliasVault
<span class="absolute -top-2 bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full font-normal">BETA</span>
<span class="ml-2 bg-primary-500 text-white text-xs px-2 py-0.5 rounded-full font-normal sm:-top-2 sm:ml-1">BETA</span>
</span>
</div>
</a>

View File

@@ -1,6 +1,7 @@
@page "/user/login"
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@layout Auth.Layout.MainLayout
@inject Config Config
@attribute [AllowAnonymous]
@using System.Text.Json
@using AliasVault.Shared.Models.WebApi.Auth
@@ -23,7 +24,11 @@
<DataAnnotationsValidator/>
<div>
<label for="two-factor-code" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Authenticator code</label>
<InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode" id="two-factor-code" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" autocomplete="off"/>
<InputNumber @bind-Value="_loginModel2Fa.TwoFactorCode"
id="two-factor-code"
@oninput="OnTwoFactorCodeInput"
class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-600 focus:border-primary-600 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
autocomplete="off"/>
<ValidationMessage For="() => _loginModel2Fa.TwoFactorCode" class="text-red-600 dark:text-red-400 text-sm mt-1"/>
</div>
<div class="flex items-start">
@@ -103,9 +108,12 @@ else
</div>
<button type="submit" class="w-full px-5 py-3 text-base font-medium text-center text-white bg-primary-700 rounded-lg 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">Login to your account</button>
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
No account yet? <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">Create new vault</a>
</div>
@if (Config.PublicRegistrationEnabled)
{
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
No account yet? <a href="/user/setup" class="text-primary-700 hover:underline dark:text-primary-500">Create new vault</a>
</div>
}
</EditForm>
}
@@ -413,4 +421,24 @@ else
return [];
}
/// <summary>
/// Auto submit the 2FA code when 6 digits are entered.
/// </summary>
/// <param name="e"></param>
private async Task OnTwoFactorCodeInput(ChangeEventArgs e)
{
if (e.Value?.ToString()?.Length >= 6)
{
// Update the blazor model with the current value.
_loginModel2Fa.TwoFactorCode = int.Parse(e.Value.ToString()!);
// Submit the form.
await Handle2Fa();
}
else
{
_serverValidationErrors.Clear();
}
}
}

View File

@@ -2,6 +2,7 @@
@using AliasVault.Client.Auth.Components
@inherits AliasVault.Client.Auth.Pages.Base.LoginBase
@layout Auth.Layout.EmptyLayout
@inject Config Config
@attribute [AllowAnonymous]
<div class="flex lg:min-h-screen bg-gray-50 dark:bg-gray-900">
@@ -22,9 +23,12 @@
Your Privacy. Protected.
</p>
<div class="space-y-4">
<a href="/user/setup" class="block w-full py-3 px-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out text-center">
Create new vault
</a>
@if (Config.PublicRegistrationEnabled)
{
<a href="/user/setup" class="block w-full py-3 px-4 bg-primary-600 hover:bg-primary-700 text-white font-semibold rounded-lg transition duration-300 ease-in-out text-center">
Create new vault
</a>
}
<a href="/user/login" class="block w-full py-3 px-4 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-800 dark:text-white font-semibold rounded-lg transition duration-300 ease-in-out text-center">
Log in with existing account
</a>

View File

@@ -14,7 +14,6 @@
@if (IsLoading) {
<BoldLoadingIndicator />
}
else if (IsWebAuthnLoading) {
<BoldLoadingIndicator />
@@ -205,7 +204,7 @@ else
}
// Check if encryption key test string is available. If not
// user should login again.
// user should log in again.
if (!await AuthService.HasEncryptionKeyTestStringAsync())
{
// Clear all tokens and redirect to login page.

View File

@@ -62,4 +62,9 @@ public class Config
/// Gets or sets the support email address that users can contact for password recovery.
/// </summary>
public string? SupportEmail { get; set; }
/// <summary>
/// Gets or sets a value indicating whether public registration is enabled.
/// </summary>
public bool PublicRegistrationEnabled { get; set; }
}

View File

@@ -7,6 +7,11 @@ ENV DOTNET_CLI_TELEMETRY_OPTOUT=1
ENV MSBUILDDEBUGPATH=/src/msbuild-logs
WORKDIR /src
# Create the debug directory
RUN mkdir -p /src/msbuild-logs
# Install Python which is required by the WebAssembly tools
RUN apt-get update && apt-get install -y python3 && apt-get clean
# Create the debug directory and install Python which is required by the WebAssembly tools
RUN mkdir -p /src/msbuild-logs && apt-get update && apt-get install -y python3 && apt-get clean
@@ -18,14 +23,25 @@ COPY ["src/AliasVault.Client/AliasVault.Client.csproj", "src/AliasVault.Client/"
RUN dotnet restore "src/AliasVault.Client/AliasVault.Client.csproj"
COPY . .
# Build and publish
# Build the Client project
WORKDIR "/src/src/AliasVault.Client"
RUN dotnet publish "AliasVault.Client.csproj" -c "$BUILD_CONFIGURATION" -o /app/publish /p:UseAppHost=false
RUN dotnet build "AliasVault.Client.csproj" -c "$BUILD_CONFIGURATION" -o /app/build
# Publish the Client project
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "AliasVault.Client.csproj" \
-c "$BUILD_CONFIGURATION" \
--no-restore \
-o /app/publish \
/p:UseAppHost=false \
/p:WasmNativeStrip=false \
/p:EmccInitialHeapSize=268435456
# Final stage
FROM nginx:1.24.0 AS final
WORKDIR /usr/share/nginx/html
COPY --from=build /app/publish/wwwroot .
COPY --from=publish /app/publish/wwwroot .
COPY /src/AliasVault.Client/nginx.conf /etc/nginx/nginx.conf
COPY /src/AliasVault.Client/entrypoint.sh /app/entrypoint.sh

View File

@@ -119,8 +119,8 @@
var response = await client.DeleteAsync($"https://api.spamok.com/v2/Email/{Email.ToLocal}/{Email.Id}");
if (response.IsSuccessStatusCode)
{
GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true);
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true);
await Close();
}
else
@@ -150,8 +150,8 @@
var response = await HttpClient.DeleteAsync($"v1/Email/{Email.Id}");
if (response.IsSuccessStatusCode)
{
GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true);
await OnEmailDeleted.InvokeAsync(Email.Id);
GlobalNotificationService.AddSuccessMessage("Email deleted successfully", true);
await Close();
}
else

View File

@@ -11,7 +11,7 @@
@inject EmailService EmailService
@using System.Timers
@inject ILogger<RecentEmails> Logger
@implements IDisposable
@implements IAsyncDisposable
@if (EmailModalVisible)
{
@@ -100,54 +100,74 @@
private bool EmailModalVisible { get; set; }
private string Error { get; set; } = string.Empty;
private bool IsRefreshing { get; set; } = true;
private bool IsLoading { get; set; } = true;
private bool IsSpamOk { get; set; } = false;
private bool IsPageVisible { get; set; } = true;
private CancellationTokenSource? PollingCancellationTokenSource { get; set; }
private const int ACTIVE_TAB_REFRESH_INTERVAL = 2000; // 2 seconds
private readonly SemaphoreSlim RefreshSemaphore = new(1, 1);
private DateTime LastRefreshTime = DateTime.MinValue;
private PeriodicTimer? _refreshTimer;
private CancellationTokenSource _cancellationTokenSource = new();
/// <summary>
/// Callback invoked by JavaScript when the page visibility changes.
/// Callback invoked by JavaScript when the page visibility changes. This is used to start/stop the polling for new emails.
/// </summary>
/// <param name="isVisible">Boolean whether the page is visible or not.</param>
/// <returns>Task.</returns>
/// <param name="isVisible">Indicates whether the page is visible or not.</param>
[JSInvokable]
public async Task OnVisibilityChange(bool isVisible)
{
IsPageVisible = isVisible;
if (isVisible)
if (isVisible && DbService.Settings.AutoEmailRefresh)
{
// Only enable auto-refresh if the setting is enabled.
if (DbService.Settings.AutoEmailRefresh)
{
await StartPolling();
}
// Refresh immediately when tab becomes visible
await ManualRefresh();
await StartPolling();
}
else
{
// Cancel polling.
if (PollingCancellationTokenSource is not null)
await StopPolling();
}
// Refresh immediately when tab becomes visible
if (isVisible)
{
await ManualRefresh();
}
}
private async Task StartPolling()
{
await StopPolling();
// Create a new CancellationTokenSource since the old one might have been cancelled
_cancellationTokenSource = new CancellationTokenSource();
_refreshTimer = new PeriodicTimer(TimeSpan.FromMilliseconds(ACTIVE_TAB_REFRESH_INTERVAL));
try
{
while (await _refreshTimer.WaitForNextTickAsync(_cancellationTokenSource.Token))
{
await PollingCancellationTokenSource.CancelAsync();
await LoadRecentEmailsAsync();
}
}
StateHasChanged();
catch (OperationCanceledException)
{
// Normal cancellation, ignore
}
}
private async Task StopPolling()
{
if (_refreshTimer is not null)
{
await _cancellationTokenSource.CancelAsync();
_refreshTimer.Dispose();
_refreshTimer = null;
}
}
/// <inheritdoc />
public void Dispose()
public async ValueTask DisposeAsync()
{
PollingCancellationTokenSource?.Cancel();
PollingCancellationTokenSource?.Dispose();
RefreshSemaphore.Dispose();
await StopPolling();
_cancellationTokenSource.Dispose();
}
/// <inheritdoc />
@@ -161,16 +181,11 @@
}
// Check if email has a known SpamOK domain, if not, don't show this component.
if (IsSpamOkDomain(EmailAddress) || IsAliasVaultDomain(EmailAddress))
{
ShowComponent = true;
}
ShowComponent = IsSpamOkDomain(EmailAddress) || IsAliasVaultDomain(EmailAddress);
IsSpamOk = IsSpamOkDomain(EmailAddress);
// Set up visibility change detection
await JsInteropService.RegisterVisibilityCallback(DotNetObjectReference.Create(this));
// Only enable auto-refresh if the setting is enabled.
if (DbService.Settings.AutoEmailRefresh)
{
await StartPolling();
@@ -206,65 +221,6 @@
IsSpamOk = IsSpamOkDomain(EmailAddress);
}
/// <summary>
/// Start the polling for new emails.
/// </summary>
/// <returns>Task.</returns>
private async Task StartPolling()
{
if (PollingCancellationTokenSource is not null)
{
await PollingCancellationTokenSource.CancelAsync();
}
PollingCancellationTokenSource = new CancellationTokenSource();
try
{
while (!PollingCancellationTokenSource.Token.IsCancellationRequested)
{
if (IsPageVisible)
{
// Only auto refresh when the tab is visible.
await RefreshWithThrottling();
await Task.Delay(ACTIVE_TAB_REFRESH_INTERVAL, PollingCancellationTokenSource.Token);
}
}
}
catch (OperationCanceledException)
{
// Normal cancellation, ignore
}
}
/// <summary>
/// Refresh the emails with throttling to prevent multiple refreshes at the same time.
/// </summary>
/// <returns></returns>
private async Task RefreshWithThrottling()
{
if (!await RefreshSemaphore.WaitAsync(0)) // Don't wait if a refresh is in progress
{
return;
}
try
{
var timeSinceLastRefresh = DateTime.UtcNow - LastRefreshTime;
if (timeSinceLastRefresh.TotalMilliseconds < ACTIVE_TAB_REFRESH_INTERVAL)
{
return;
}
await LoadRecentEmailsAsync();
LastRefreshTime = DateTime.UtcNow;
}
finally
{
RefreshSemaphore.Release();
}
}
/// <summary>
/// Returns true if the email address is from a known SpamOK domain.
/// </summary>
@@ -305,9 +261,6 @@
return;
}
Error = string.Empty;
StateHasChanged();
// Get email prefix, which is the part before the @ symbol.
string emailPrefix = EmailAddress.Split('@')[0];
@@ -319,6 +272,8 @@
{
await LoadAliasVaultEmails();
}
StateHasChanged();
}
/// <summary>
@@ -440,6 +395,8 @@
}
MailboxEmails = await EmailService.DecryptEmailList(MailboxEmails);
Error = string.Empty;
}
/// <summary>

View File

@@ -154,7 +154,7 @@
// Error saving.
IsCreating = false;
GlobalLoadingSpinner.Hide();
GlobalNotificationService.AddErrorMessage("Error saving credentials. Please try again.", true);
GlobalNotificationService.AddErrorMessage("Error creating a new credential. Please try again (later) or log-out and in again.", true);
return;
}

View File

@@ -8,7 +8,7 @@
<input
id="searchWidget"
type="text"
placeholder="Type here to search"
placeholder="Search for a service..."
autocomplete="off"
class="w-full px-4 py-2 text-gray-700 bg-white border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:focus:ring-primary-500"
@bind-value="SearchTerm"

View File

@@ -82,10 +82,15 @@ else
}
GlobalLoadingSpinner.Show();
await CredentialService.SoftDeleteEntryAsync(Id);
GlobalNotificationService.AddSuccessMessage("Credentials entry successfully deleted.");
GlobalLoadingSpinner.Hide();
if (await CredentialService.SoftDeleteEntryAsync(Id))
{
GlobalNotificationService.AddSuccessMessage("Credentials entry successfully deleted.");
}
else {
GlobalNotificationService.AddErrorMessage("Error saving database.", true);
}
GlobalLoadingSpinner.Hide();
NavigationManager.NavigateTo("/");
}

View File

@@ -173,6 +173,7 @@ else
var vaultPasswordChangeObject = new VaultPasswordChangeRequest
{
Username = username,
Blob = vault.Blob,
Version = vault.Version,
CurrentRevisionNumber = vault.CurrentRevisionNumber,

View File

@@ -4,7 +4,7 @@
The recovery codes below are used to access your account in case you lose access to your authenticator device.
Make a photo or write them down and store them in a secure location. Do not share them with anyone.
</div>
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4">
<div class="bg-primary-100 border-l-4 border-primary-500 text-primary-700 p-4 mb-4 dark:bg-gray-700 dark:border-primary-500 dark:text-gray-200">
<p class="font-semibold">
Store these recovery codes in a safe place.
</p>
@@ -18,7 +18,7 @@
@foreach (var recoveryCode in RecoveryCodes)
{
<div>
<code class="block p-2">@recoveryCode</code>
<code class="block p-2 bg-gray-100 dark:bg-gray-700 dark:text-gray-200">@recoveryCode</code>
</div>
}
</div>

View File

@@ -50,11 +50,11 @@
/// <returns>A task representing the asynchronous operation.</returns>
private async Task LoadData()
{
// Currently with SQLite, we have to load the data sequentially as otherwise we get database locks.
// When switched over to PostgreSQL, we can load the data concurrently.
await TwoFactorSection!.LoadData();
await QuickVaultUnlockSection!.LoadData();
await SessionsSection!.LoadData();
await RecentAuthLogsSection!.LoadData();
await Task.WhenAll(
TwoFactorSection!.LoadData(),
QuickVaultUnlockSection!.LoadData(),
SessionsSection!.LoadData(),
RecentAuthLogsSection!.LoadData()
);
}
}

View File

@@ -134,9 +134,15 @@ else if (!string.IsNullOrEmpty(ImportSuccessMessage))
}
// Save the database.
await DbService.SaveDatabaseAsync();
ImportSuccessMessage = $"Successfully imported {importedCredentials.Count} credentials.";
var success = await DbService.SaveDatabaseAsync();
if (success)
{
ImportSuccessMessage = $"Successfully imported {importedCredentials.Count} credentials.";
}
else
{
ImportErrorMessage = "Error saving database.";
}
}
catch (Exception ex)
{

View File

@@ -74,10 +74,16 @@
if (await DbService.MigrateDatabaseAsync())
{
// Save the database to the server.
await DbService.SaveDatabaseAsync();
// Migration successful.
GlobalNotificationService.AddSuccessMessage("Vault upgrade successful.", true);
if (await DbService.SaveDatabaseAsync())
{
// Migration successful.
GlobalNotificationService.AddSuccessMessage("Vault upgrade successful.", true);
}
else
{
// Migration failed
ErrorMessage = "Database upgrade successful but failed to save to server. Please try again or contact support.";
}
}
else
{

View File

@@ -11,7 +11,6 @@ using System.Net.Http.Json;
using System.Text.Json;
using AliasVault.Shared.Models.WebApi.Auth;
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
/// <summary>
@@ -34,6 +33,14 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
/// </summary>
private const string EncryptionTestString = "aliasvault-test-string";
/// <summary>
/// The username of the currently logged-in user to prevent any conflicts during future vault saves.
/// </summary>
private string _username = string.Empty;
/// <summary>
/// The encryption key used to encrypt and decrypt the vault data.
/// </summary>
private byte[] _encryptionKey = new byte[32];
/// <summary>
@@ -42,7 +49,6 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
/// <returns>The new access token.</returns>
public async Task<string?> RefreshTokenAsync()
{
// Your logic to get the refresh token and request a new access token
var accessToken = await GetAccessTokenAsync();
var refreshToken = await GetRefreshTokenAsync();
var tokenInput = new TokenModel { Token = accessToken, RefreshToken = refreshToken };
@@ -73,6 +79,26 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
return null;
}
/// <summary>
/// Retrieves the username of the currently logged-in user.
/// </summary>
/// <returns>The currently logged-in user's username.</returns>
public string GetUsername()
{
return _username;
}
/// <summary>
/// Stores the username of the vault owner in local memory. This value will be sent to the server during
/// vault updates to ensure that the API is updating the correct vault of the correct user preventing any conflicts
/// or vault corruption.
/// </summary>
/// <param name="username">The username of the currently logged-in user and owner of the vault being loaded.</param>
public void StoreUsername(string? username)
{
_username = username ?? string.Empty;
}
/// <summary>
/// Retrieves the stored access token asynchronously.
/// </summary>
@@ -292,6 +318,7 @@ public sealed class AuthService(HttpClient httpClient, ILocalStorageService loca
}
// Remove the tokens from local storage.
_username = string.Empty;
await localStorage.RemoveItemAsync(AccessTokenKey);
await localStorage.RemoveItemAsync(RefreshTokenKey);
}

View File

@@ -66,6 +66,8 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP
return (false, "An error occurred during registration.");
}
// Store username of the loaded vault in memory to send to server as sanity check when updating the vault later.
authService.StoreUsername(username);
await authService.StoreEncryptionKeyAsync(passwordHash);
await authService.StoreAccessTokenAsync(tokenObject.Token);
await authService.StoreRefreshTokenAsync(tokenObject.RefreshToken);

View File

@@ -164,9 +164,11 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
// Add password.
login.Passwords.Add(loginObject.Passwords.First());
if (saveToDb)
// Save the database to the server if saveToDb is true.
if (saveToDb && !await dbService.SaveDatabaseAsync())
{
await dbService.SaveDatabaseAsync();
// If saving database to server failed, return empty guid to indicate error.
return Guid.Empty;
}
return login.Id;
@@ -255,7 +257,12 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
}
}
await dbService.SaveDatabaseAsync();
// Save the database to the server.
if (!await dbService.SaveDatabaseAsync())
{
// If saving database failed, return empty guid to indicate error.
return Guid.Empty;
}
return login.Id;
}
@@ -332,8 +339,8 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
/// is required in order to synchronize the deletion of entries across multiple client vault versions.
/// </summary>
/// <param name="id">Id of alias to delete.</param>
/// <returns>Task.</returns>
public async Task SoftDeleteEntryAsync(Guid id)
/// <returns>Bool which indicates if deletion and saving database was successful.</returns>
public async Task<bool> SoftDeleteEntryAsync(Guid id)
{
var context = await dbService.GetDbContextAsync();
@@ -357,7 +364,7 @@ public sealed class CredentialService(HttpClient httpClient, DbService dbService
service.IsDeleted = true;
service.UpdatedAt = DateTime.UtcNow;
await dbService.SaveDatabaseAsync();
return await dbService.SaveDatabaseAsync();
}
/// <summary>

View File

@@ -115,12 +115,14 @@ public sealed class DbService : IDisposable
}
var sqlConnections = new List<SqliteConnection>();
_logger.LogInformation("Merging databases...");
// Decrypt and instantiate each vault as a separate in-memory SQLite database.
foreach (var vault in vaultsToMerge.Vaults)
{
// Store username of the loaded vault in memory to send to server as sanity check when updating the vault later.
_authService.StoreUsername(vault.Username);
var decryptedBase64String = await _jsInteropService.SymmetricDecrypt(vault.Blob, _authService.GetEncryptionKeyAsBase64Async());
_logger.LogInformation("Decrypted vault {VaultUpdatedAt}.", vault.UpdatedAt);
@@ -140,7 +142,7 @@ public sealed class DbService : IDisposable
await command.ExecuteNonQueryAsync();
}
// Merge each database into the base.
// Merge every remote database into the current database.
foreach (var connection in sqlConnections)
{
foreach (var table in tables)
@@ -190,9 +192,7 @@ public sealed class DbService : IDisposable
_logger.LogInformation("Databases merged successfully.");
// Save the newly merged database to the server.
await SaveDatabaseAsync();
return true;
return await SaveDatabaseAsync();
}
catch (Exception ex)
{
@@ -248,8 +248,8 @@ public sealed class DbService : IDisposable
/// <summary>
/// Saves the database to the remote server.
/// </summary>
/// <returns>Task.</returns>
public async Task SaveDatabaseAsync()
/// <returns>Bool which indicates if saving database to server was successful.</returns>
public async Task<bool> SaveDatabaseAsync()
{
// Set the initial state of the database service.
_state.UpdateState(DbServiceState.DatabaseStatus.SavingToServer);
@@ -266,6 +266,8 @@ public sealed class DbService : IDisposable
_logger.LogInformation("Database successfully saved to server.");
_state.UpdateState(DbServiceState.DatabaseStatus.Ready);
}
return success;
}
/// <summary>
@@ -374,12 +376,14 @@ public sealed class DbService : IDisposable
/// <returns>Vault object.</returns>
public async Task<Vault> PrepareVaultForUploadAsync(string encryptedDatabase)
{
var username = _authService.GetUsername();
var databaseVersion = await GetCurrentDatabaseVersionAsync();
var encryptionKey = await GetOrCreateEncryptionKeyAsync();
var credentialsCount = await _dbContext.Credentials.Where(x => !x.IsDeleted).CountAsync();
var emailAddresses = await GetEmailClaimListAsync();
return new Vault
{
Username = username,
Blob = encryptedDatabase,
Version = databaseVersion,
CurrentRevisionNumber = _vaultRevisionNumber,
@@ -570,6 +574,9 @@ public sealed class DbService : IDisposable
var vault = response.Vault!;
_vaultRevisionNumber = vault.CurrentRevisionNumber;
// Store username of the loaded vault in memory to send to server as sanity check when updating the vault later.
_authService.StoreUsername(vault.Username);
// Check if vault blob is empty, if so, we don't need to do anything and the initial vault created
// on client is sufficient.
if (string.IsNullOrEmpty(vault.Blob))
@@ -636,8 +643,7 @@ public sealed class DbService : IDisposable
if (vaultUpdateResponse.Status == VaultStatus.MergeRequired)
{
_state.UpdateState(DbServiceState.DatabaseStatus.MergeRequired);
await MergeDatabasesAsync();
return false;
return await MergeDatabasesAsync();
}
_vaultRevisionNumber = vaultUpdateResponse.NewRevisionNumber;
@@ -719,7 +725,11 @@ public sealed class DbService : IDisposable
if (deleteCount > 0)
{
// Save the database to the server to persist the cleanup.
await SaveDatabaseAsync();
var success = await SaveDatabaseAsync();
if (!success)
{
throw new DataException("Error saving database to server after attachment deletion.");
}
}
}

View File

@@ -8,6 +8,7 @@
namespace AliasVault.Client.Services;
using System;
using System.Data;
using System.Text.Json;
using System.Threading.Tasks;
using AliasClientDb;
@@ -263,6 +264,10 @@ public sealed class SettingsService
// is returned by subsequent local reads.
_settings[key] = value;
await _dbService.SaveDatabaseAsync();
var success = await _dbService.SaveDatabaseAsync();
if (!success)
{
throw new DataException("Error saving database to server after setting update.");
}
}
}

View File

@@ -41,5 +41,8 @@ else
sed -i "s|\"SupportEmail\": \".*\"|\"SupportEmail\": \"\"|g" /usr/share/nginx/html/appsettings.json
fi
# Update public registration enabled in appsettings.json
sed -i "s|\"PublicRegistrationEnabled\": \".*\"|\"PublicRegistrationEnabled\": \"$PUBLIC_REGISTRATION_ENABLED\"|g" /usr/share/nginx/html/appsettings.json
# Start the application
nginx -g "daemon off;"

View File

@@ -1,5 +1,6 @@
{
"ApiUrl": "http://localhost:5092",
"PrivateEmailDomains": ["example.tld"],
"SupportEmail": "support@example.tld"
"SupportEmail": "support@example.tld",
"PublicRegistrationEnabled": "true"
}

View File

@@ -638,10 +638,6 @@ video {
bottom: 0.25rem;
}
.-top-2 {
top: -0.5rem;
}
.bottom-0 {
bottom: 0px;
}
@@ -682,6 +678,10 @@ video {
z-index: 10;
}
.z-20 {
z-index: 20;
}
.z-30 {
z-index: 30;
}
@@ -690,10 +690,6 @@ video {
z-index: 50;
}
.z-20 {
z-index: 20;
}
.col-span-1 {
grid-column: span 1 / span 1;
}
@@ -1024,14 +1020,14 @@ video {
max-width: 1536px;
}
.max-w-xl {
max-width: 36rem;
}
.max-w-screen-xl {
max-width: 1280px;
}
.max-w-xl {
max-width: 36rem;
}
.flex-shrink-0 {
flex-shrink: 0;
}
@@ -1640,10 +1636,6 @@ video {
padding-bottom: 7rem;
}
.pb-4 {
padding-bottom: 1rem;
}
.pb-8 {
padding-bottom: 2rem;
}
@@ -1908,11 +1900,6 @@ video {
color: rgb(133 77 14 / var(--tw-text-opacity));
}
.text-amber-500 {
--tw-text-opacity: 1;
color: rgb(245 158 11 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
@@ -2083,16 +2070,16 @@ video {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.hover\:from-primary-600:hover {
--tw-gradient-from: #d68338 var(--tw-gradient-from-position);
--tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position);
@@ -2138,6 +2125,11 @@ video {
color: rgb(184 112 47 / var(--tw-text-opacity));
}
.hover\:text-red-200:hover {
--tw-text-opacity: 1;
color: rgb(254 202 202 / var(--tw-text-opacity));
}
.hover\:text-red-700:hover {
--tw-text-opacity: 1;
color: rgb(185 28 28 / var(--tw-text-opacity));
@@ -2148,11 +2140,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.hover\:text-red-200:hover {
--tw-text-opacity: 1;
color: rgb(254 202 202 / var(--tw-text-opacity));
}
.hover\:underline:hover {
text-decoration-line: underline;
}
@@ -2303,14 +2290,19 @@ video {
border-color: rgb(239 68 68 / var(--tw-border-opacity));
}
.dark\:border-red-800:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity));
}
.dark\:border-yellow-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(234 179 8 / var(--tw-border-opacity));
}
.dark\:border-red-800:is(.dark *) {
.dark\:border-primary-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity));
border-color: rgb(244 149 65 / var(--tw-border-opacity));
}
.dark\:bg-blue-800:is(.dark *) {
@@ -2378,6 +2370,11 @@ video {
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.dark\:bg-red-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
}
.dark\:bg-white:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -2393,11 +2390,6 @@ video {
background-color: rgb(113 63 18 / var(--tw-bg-opacity));
}
.dark\:bg-red-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(127 29 29 / var(--tw-bg-opacity));
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
@@ -2652,6 +2644,14 @@ video {
}
@media (min-width: 640px) {
.sm\:absolute {
position: absolute;
}
.sm\:-top-2 {
top: -0.5rem;
}
.sm\:col-span-3 {
grid-column: span 3 / span 3;
}
@@ -2660,6 +2660,10 @@ video {
margin-bottom: 0px;
}
.sm\:ml-1 {
margin-left: 0.25rem;
}
.sm\:mr-4 {
margin-right: 1rem;
}
@@ -2790,6 +2794,10 @@ video {
margin-bottom: 0px;
}
.lg\:mb-16 {
margin-bottom: 4rem;
}
.lg\:mr-8 {
margin-right: 2rem;
}
@@ -2806,10 +2814,6 @@ video {
margin-top: 4rem;
}
.lg\:mb-16 {
margin-bottom: 4rem;
}
.lg\:block {
display: block;
}
@@ -2893,10 +2897,6 @@ video {
padding-bottom: 3rem;
}
.lg\:pb-16 {
padding-bottom: 4rem;
}
.lg\:pb-4 {
padding-bottom: 1rem;
}

View File

@@ -53,6 +53,7 @@
<div class="mt-4 text-center">
<p id="security-quote" class="text-sm text-primary-600 italic"></p>
</div>
<div id="error-message" class="hidden text-red-600 dark:text-red-400 mt-4"></div>
</div>
</div>
</div>
@@ -144,7 +145,7 @@
clearInterval(intervalId);
} else if (elapsedTime % 1000 < checkInterval) {
if (!('WebAssembly' in window)) {
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
clearInterval(intervalId);
}
}
@@ -157,7 +158,6 @@
const errorMessageElement = document.getElementById('error-message');
const showError = (message) => {
loadingScreen.querySelector('.inner').classList.add('hidden');
errorMessageElement.textContent = message;
errorMessageElement.classList.remove('hidden');
document.querySelector('.loading-progress-text').classList.add('hidden');
@@ -167,14 +167,14 @@
// Listen for unhandled errors
window.addEventListener('error', function(event) {
if (event.error && event.error.message && event.error.message.includes('WebAssembly')) {
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
}
});
// Listen for unhandled promise rejections
window.addEventListener('unhandledrejection', function(event) {
if (event.reason && event.reason.message && event.reason.message.includes('WebAssembly')) {
showError("AliasVault requires WebAssembly, which this browser does not support. Please use a modern browser that supports WebAssembly.");
showError("AliasVault requires WebAssembly, which this browser does not support. Try using a more modern browser that supports WebAssembly.");
}
});

View File

@@ -31,6 +31,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -49,4 +50,8 @@
<ProjectReference Include="..\..\Utilities\AliasVault.WorkerStatus\AliasVault.WorkerStatus.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\PostgresqlMigrations\" />
</ItemGroup>
</Project>

View File

@@ -131,6 +131,11 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
/// </summary>
public DbSet<ServerSetting> ServerSettings { get; set; } = null!;
/// <summary>
/// Gets or sets the TaskRunnerJobs DbSet.
/// </summary>
public DbSet<TaskRunnerJob> TaskRunnerJobs { get; set; }
/// <summary>
/// The OnModelCreating method.
/// </summary>
@@ -216,10 +221,10 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
// Note: when a user is deleted the email claims user FK's should be set to NULL
// so the claims themselves are preserved to prevent re-use of the email address.
modelBuilder.Entity<UserEmailClaim>()
.HasOne(l => l.User)
.WithMany(c => c.EmailClaims)
.HasForeignKey(l => l.UserId)
.OnDelete(DeleteBehavior.ClientSetNull);
.HasOne(e => e.User)
.WithMany(u => u.EmailClaims)
.HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.SetNull);
// Configure Email - UserEncryptionKey relationship
modelBuilder.Entity<Email>()
@@ -235,32 +240,4 @@ public class AliasServerDbContext : WorkerStatusDbContext, IDataProtectionKeyCon
.HasForeignKey(l => l.UserId)
.OnDelete(DeleteBehavior.Cascade);
}
/// <summary>
/// Sets up the connection string if it is not already configured.
/// </summary>
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
// Add SQLite connection with enhanced settings
var connectionString = configuration.GetConnectionString("AliasServerDbContext") +
";Mode=ReadWriteCreate;Cache=Shared" +
";Journal Mode=WAL" +
";Synchronous=Normal" +
";Busy Timeout=30000";
optionsBuilder
.UseSqlite(connectionString, options => options.CommandTimeout(60))
.UseLazyLoadingProxies();
}
}

View File

@@ -0,0 +1,85 @@
//-----------------------------------------------------------------------
// <copyright file="AliasServerDbContextPostgresql.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 AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Configuration;
/// <summary>
/// PostgreSQL implementation of the AliasServerDbContext.
/// </summary>
public class AliasServerDbContextPostgresql : AliasServerDbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AliasServerDbContextPostgresql"/> class.
/// </summary>
public AliasServerDbContextPostgresql()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="AliasServerDbContextPostgresql"/> class.
/// </summary>
/// <param name="options">DbContextOptions.</param>
public AliasServerDbContextPostgresql(DbContextOptions<AliasServerDbContext> options)
: base(options)
{
}
/// <summary>
/// Sets up the connection string if it is not already configured.
/// </summary>
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
if (optionsBuilder.IsConfigured)
{
return;
}
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
// Add SQLite connection with enhanced settings
var connectionString = configuration.GetConnectionString("AliasServerDbContext");
optionsBuilder
.UseNpgsql(connectionString, options => options.CommandTimeout(60))
.UseLazyLoadingProxies();
}
/// <inheritdoc/>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure all DateTime properties to use timestamp with time zone in UTC
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entityType.GetProperties())
{
if (property.ClrType == typeof(DateTime) || property.ClrType == typeof(DateTime?))
{
property.SetColumnType("timestamp with time zone");
// Add value converter for DateTime properties
var converter = new ValueConverter<DateTime, DateTime>(
v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(),
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
property.SetValueConverter(converter);
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
//-----------------------------------------------------------------------
// <copyright file="AliasServerDbContextSqlite.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 AliasServerDb;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
/// <summary>
/// SQLite implementation of the AliasServerDbContext.
/// </summary>
public class AliasServerDbContextSqlite : AliasServerDbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="AliasServerDbContextSqlite"/> class.
/// </summary>
public AliasServerDbContextSqlite()
{
SetPragmaSettings();
}
/// <summary>
/// Initializes a new instance of the <see cref="AliasServerDbContextSqlite"/> class.
/// </summary>
/// <param name="options">DbContextOptions.</param>
public AliasServerDbContextSqlite(DbContextOptions<AliasServerDbContext> options)
: base(options)
{
SetPragmaSettings();
}
/// <summary>
/// The OnModelCreating method.
/// </summary>
/// <param name="modelBuilder">ModelBuilder instance.</param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Set TEXT as default column type for string properties because
// SQLite does not support varchar(max).
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
foreach (var property in entity.GetProperties())
{
// SQLite does not support varchar(max) so we use TEXT.
if (property.ClrType == typeof(string) && property.GetMaxLength() == null)
{
property.SetColumnType("TEXT");
}
}
}
}
/// <summary>
/// Sets up the connection string if it is not already configured.
/// </summary>
/// <param name="optionsBuilder">DbContextOptionsBuilder instance.</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured)
{
return;
}
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json")
.Build();
// Add SQLite connection with enhanced settings
var connectionString = configuration.GetConnectionString("AliasServerDbContext") +
";Mode=ReadWriteCreate;Cache=Shared";
optionsBuilder
.UseSqlite(connectionString, options => options.CommandTimeout(60))
.UseLazyLoadingProxies();
}
/// <summary>
/// Sets up the PRAGMA settings for SQLite.
/// </summary>
private void SetPragmaSettings()
{
var connection = Database.GetDbConnection();
if (connection.State != System.Data.ConnectionState.Open)
{
connection.Open();
}
using (var command = connection.CreateCommand())
{
// Increase busy timeout
command.CommandText = @"
PRAGMA busy_timeout = 30000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA temp_store = MEMORY;
PRAGMA mmap_size = 1073741824;";
command.ExecuteNonQuery();
}
}
}

View File

@@ -24,6 +24,11 @@ public class AliasVaultUser : IdentityUser
/// </summary>
public DateTime PasswordChangedAt { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user is blocked and should not be able to log in.
/// </summary>
public bool Blocked { get; set; }
/// <summary>
/// Gets or sets updated timestamp.
/// </summary>

View File

@@ -47,6 +47,11 @@ public enum AuthFailureReason
/// </summary>
InvalidRefreshToken = 6,
/// <summary>
/// Indicates that the account is manually blocked by an administrator.
/// </summary>
AccountBlocked = 7,
/// <summary>
/// Indicates that the failure reason was unknown.
/// </summary>
@@ -86,7 +91,6 @@ public class AuthLog
/// Gets or sets the type of authentication event (e.g., Login, Logout, FailedLogin).
/// </summary>
[Required]
[Column(TypeName = "nvarchar(50)")]
public AuthEventType EventType { get; set; }
/// <summary>

View File

@@ -7,9 +7,6 @@
namespace AliasServerDb.Configuration;
using System.Data.Common;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -22,39 +19,58 @@ public static class DatabaseConfiguration
/// Configures SQLite for use with Entity Framework Core.
/// </summary>
/// <param name="services">The IServiceCollection to add the DbContext to.</param>
/// <param name="configuration">The IConfiguration to use for the connection string.</param>
/// <returns>The IServiceCollection for method chaining.</returns>
public static IServiceCollection AddAliasVaultSqliteConfiguration(this IServiceCollection services)
public static IServiceCollection AddAliasVaultDatabaseConfiguration(this IServiceCollection services, IConfiguration configuration)
{
var serviceProvider = services.BuildServiceProvider();
var configuration = serviceProvider.GetRequiredService<IConfiguration>();
// Check for environment variables first, then fall back to configuration
var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__AliasServerDbContext");
var dbProvider = Environment.GetEnvironmentVariable("DatabaseProvider")?.ToLower()
?? configuration.GetValue<string>("DatabaseProvider")?.ToLower()
?? "postgresql";
var connectionString = configuration.GetConnectionString("AliasServerDbContext");
if (string.IsNullOrEmpty(connectionString))
// Create a new configuration if we have environment-provided values
if (!string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException("Connection string 'AliasServerDbContext' not found.");
var configDictionary = new Dictionary<string, string?>
{
["ConnectionStrings:AliasServerDbContext"] = connectionString,
["DatabaseProvider"] = dbProvider,
};
var configurationBuilder = new ConfigurationBuilder()
.AddInMemoryCollection(configDictionary);
// Only add the original configuration after our environment variables
// This ensures environment variables take precedence
configurationBuilder.AddConfiguration(configuration).Build();
}
var sqliteConnectionStringBuilder = new SqliteConnectionStringBuilder(connectionString)
// Add custom DbContextFactory registration which supports multiple database providers
switch (dbProvider)
{
Cache = SqliteCacheMode.Private,
Mode = SqliteOpenMode.ReadWriteCreate,
};
case "postgresql":
services.AddSingleton<IAliasServerDbContextFactory, PostgresqlDbContextFactory>();
break;
default:
services.AddSingleton<IAliasServerDbContextFactory, SqliteDbContextFactory>();
break;
}
services.AddDbContextFactory<AliasServerDbContext>(options =>
// Updated DbContextFactory registration
services.AddDbContextFactory<AliasServerDbContext>((sp, options) =>
{
options.UseSqlite(CreateAndConfigureSqliteConnection(sqliteConnectionStringBuilder.ConnectionString), sqliteOptions =>
{
sqliteOptions.CommandTimeout(60);
}).UseLazyLoadingProxies();
var factory = sp.GetRequiredService<IAliasServerDbContextFactory>();
factory.ConfigureDbContextOptions(options);
});
// Add scoped DbContext registration based on the factory
services.AddScoped<AliasServerDbContext>(sp =>
{
var factory = sp.GetRequiredService<IAliasServerDbContextFactory>();
return factory.CreateDbContext();
});
return services;
}
private static SqliteConnection CreateAndConfigureSqliteConnection(string connectionString)
{
var connection = new SqliteConnection(connectionString);
connection.Open();
return connection;
}
}

View File

@@ -0,0 +1,6 @@
FROM postgres:16-alpine
# Add any custom PostgreSQL configurations if needed
COPY src/Databases/AliasServerDb/postgresql.conf /etc/postgresql/postgresql.conf
CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"]

View File

@@ -0,0 +1,35 @@
//-----------------------------------------------------------------------
// <copyright file="IAliasServerDbContextFactory.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 AliasServerDb;
using Microsoft.EntityFrameworkCore;
/// <summary>
/// The AliasServerDbContextFactory interface.
/// </summary>
public interface IAliasServerDbContextFactory
{
/// <summary>
/// Creates a new AliasServerDbContext.
/// </summary>
/// <returns>The AliasServerDbContext.</returns>
AliasServerDbContext CreateDbContext();
/// <summary>
/// Configures the DbContext options.
/// </summary>
/// <param name="optionsBuilder">The DbContextOptionsBuilder.</param>
void ConfigureDbContextOptions(DbContextOptionsBuilder optionsBuilder);
/// <summary>
/// Creates a new AliasServerDbContext asynchronously.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the AliasServerDbContext.</returns>
Task<AliasServerDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default);
}

Some files were not shown because too many files have changed in this diff Show More