Compare commits

...

126 Commits

Author SHA1 Message Date
Chris Collins
c8ed7eff7e Merge branch 'prerel-1.7.0.1' into 'main'
Detect changes to cron schedules

See merge request personal/wedding-share!83
2025-08-03 17:59:57 +01:00
Cirx08
3943bf3dc6 Detect changes to cron schedules 2025-08-03 17:57:15 +01:00
Chris Collins
0714b93dc9 Merge branch 'prerel-1.6.11.2' into 'main'
Prerel 1.7.0

See merge request personal/wedding-share!82
2025-08-01 17:36:42 +01:00
Chris Collins
d391709be6 Prerel 1.7.0 2025-08-01 17:36:42 +01:00
Chris Collins
adb9d5242e Merge branch 'prerel-1.6.11.1' into 'main'
- Fix to remove async from RequiresSecretKeyAttribute as it was allowing the...

See merge request personal/wedding-share!81
2025-07-30 21:24:16 +01:00
Chris Collins
b72e493675 - Fix to remove async from RequiresSecretKeyAttribute as it was allowing the... 2025-07-30 21:24:15 +01:00
Chris Collins
907c460d97 Merge branch 'prerel-1.6.11' into 'main'
Prerel 1.6.11

See merge request personal/wedding-share!80
2025-07-29 22:03:24 +01:00
Chris Collins
98572919b2 Prerel 1.6.11 2025-07-29 22:03:24 +01:00
Chris Collins
febd844c91 Merge branch 'prerel-1.6.10.8' into 'main'
Added cookie consent when policy is disabled

See merge request personal/wedding-share!79
2025-07-18 21:06:19 +01:00
Cirx08
bda3c61f7f Added cookie consent when policy is disabled 2025-07-18 21:04:01 +01:00
Chris Collins
254734bdc1 Merge branch 'prerel-1.6.10.7' into 'main'
1.6.10.7

See merge request personal/wedding-share!78
2025-07-18 17:03:05 +01:00
Chris Collins
829281b616 1.6.10.7 2025-07-18 17:03:05 +01:00
Chris Collins
dff86d9232 Merge branch 'prerel-1.6.10.6' into 'main'
Prerel 1.6.10.6

See merge request personal/wedding-share!77
2025-07-18 11:45:26 +01:00
Chris Collins
50668efc1b Prerel 1.6.10.6 2025-07-18 11:45:26 +01:00
Chris Collins
845634e8f1 Merge branch 'prerel-1.6.10.5' into 'main'
Localization fixes

See merge request personal/wedding-share!76
2025-07-15 20:27:49 +01:00
Cirx08
9ad2253972 Localization fixes 2025-07-15 20:03:56 +01:00
Chris Collins
a155c3a442 Merge branch 'prerel-1.6.10.4' into 'main'
Prerel 1.6.10.4

See merge request personal/wedding-share!75
2025-07-15 17:41:07 +01:00
Chris Collins
4ac9622374 Prerel 1.6.10.4 2025-07-15 17:41:07 +01:00
Chris Collins
c59e2bb3b9 Added gallery override to IncludeCulture on gallery page 2025-07-15 16:36:34 +01:00
Chris Collins
e6bb583601 Merge branch 'prerel-1.6.10.2' into 'main'
Fix for QR code culture setting

See merge request personal/wedding-share!74
2025-07-15 16:00:13 +01:00
Chris Collins
a7f1b0c906 Fix for QR code culture setting 2025-07-15 15:52:03 +01:00
Chris Collins
1607961543 Bumped version number 2025-07-15 10:42:45 +01:00
Christopher Collins
e1d2e5e4d9 Bumped version number 2025-07-15 10:32:12 +01:00
Christopher Collins
a30eb683d6 Fixes for gallery creation and SQLite connection test 2025-07-15 09:59:35 +01:00
Chris Collins
e454d46e49 Merge branch 'prerel-1.6.9' into 'main'
Migrated settings to Ids instead of gallery name

See merge request personal/wedding-share!69
2025-07-14 19:22:39 +01:00
Chris Collins
067f631c6c Migrated settings to Ids instead of gallery name 2025-07-14 19:22:38 +01:00
Chris Collins
3b36854a21 Merge branch 'prerel-1.6.9.3' into 'main'
Halt application if DB connection issues

See merge request personal/wedding-share!73
2025-07-13 10:51:18 +01:00
Cirx08
421d8afa36 Halt application if DB connection issues 2025-07-13 10:44:17 +01:00
Chris Collins
5ad8ccdf2f Merge branch 'prerel-1.6.9.2' into 'main'
Bug fix for MySQL DBUP init scripts

See merge request personal/wedding-share!72
2025-07-12 18:02:46 +01:00
Cirx08
313152ed92 Bug fix for MySQL DBUP init scripts 2025-07-12 17:52:55 +01:00
Chris Collins
e5c6578264 Added alias to MySQL query 2025-07-11 16:11:46 +01:00
Chris Collins
9e7ff7fff7 Merge branch 'prerel-1.6.8.5' into 'main'
Prerel 1.6.9

See merge request personal/wedding-share!71
2025-07-09 21:45:14 +01:00
Chris Collins
f0f57cf093 Prerel 1.6.9 2025-07-09 21:45:14 +01:00
Chris Collins
30db4523f4 Merge branch 'prerel-1.6.8.4' into 'main'
Allow special characters in identity while preventing XSS

See merge request personal/wedding-share!70
2025-07-07 21:55:07 +01:00
Chris Collins
894b4b37e7 Allow special characters in identity while preventing XSS 2025-07-07 21:55:07 +01:00
Chris Collins
17832f1ac1 Merge branch 'prerel-1.6.8.3' into 'main'
Simplified file uploader

See merge request personal/wedding-share!68
2025-06-27 16:49:43 +01:00
Cirx08
1adbb294aa Simplified file uploader 2025-06-25 21:16:18 +01:00
Chris Collins
804ab650d7 Merge branch 'prerel-1.6.8.2' into 'main'
Updated IT translations

See merge request personal/wedding-share!67
2025-06-18 21:19:46 +01:00
Cirx08
efd5dd9bd5 Updated IT translations 2025-06-18 21:15:48 +01:00
Chris Collins
af13edbf0c Merge branch 'prerel-1.6.8.1' into 'main'
Add support for older instances using the Admin keys

See merge request personal/wedding-share!66
2025-06-16 22:42:53 +01:00
Cirx08
bb79429079 Add support for older instances using the Admin keys 2025-06-16 22:40:46 +01:00
Chris Collins
aa1debcba3 Merge branch 'prerel-30-organise-thumbnails-in-folders' into 'main'
Moved thumbnails directory

See merge request personal/wedding-share!65
2025-06-14 11:43:02 +01:00
Chris Collins
dd757c2aca Moved thumbnails directory 2025-06-14 11:43:02 +01:00
Chris Collins
aeb53708de Merge branch 'prerel-italian-translations' into 'main'
Updated translations provided by CoffeePerry

See merge request personal/wedding-share!64
2025-06-11 21:10:41 +01:00
Chris Collins
aac4937f35 Updated translations provided by CoffeePerry 2025-06-11 21:10:41 +01:00
Chris Collins
d0a58d5d08 Merge branch 'prerel-29-migrate-admin-controller-to-account-controller' into 'main'
Replaced the Admin controller with the Account controller

See merge request personal/wedding-share!63
2025-06-08 20:16:37 +01:00
Cirx08
aa54385ce2 Replaced the Admin controller with the Account controller 2025-06-08 17:05:35 +01:00
Chris Collins
36d0e4ac6a Merge branch 'prerel-1.6.6.8' into 'main'
Updated `Gallery_Key_Placeholder` value to be less confusing

See merge request personal/wedding-share!62
2025-06-03 15:02:16 +01:00
Chris
db5f811ae4 Updated Gallery_Key_Placeholder value to be less confusing 2025-06-03 14:59:59 +01:00
Chris Collins
a056596325 Bumped version number to 1.6.6.7. 2025-05-22 10:40:31 +01:00
Cirx08
d8edf39aab Updated identity check text to prevent wrapping 2025-05-21 18:49:37 +01:00
Cirx08
33d0b6be0d Added sponsors link options to popup 2025-05-20 10:37:55 +01:00
Cirx08
7b6bc83953 Fixed sponsors list display issues 2025-05-20 10:17:29 +01:00
Cirx08
ec71b8a43a Removed Github profile link from BuyMeACoffee sponsor cards 2025-05-19 23:22:20 +01:00
Cirx08
84ee089e3d Changed "Identity Check" to "Guest Name" and increased default upload size from 10MB to 50MB 2025-05-19 15:47:28 +01:00
Cirx08
6a330a7215 Bumped version number 2025-05-19 15:27:15 +01:00
Cirx08
d6bb68ec5d Prepend filename with uploader name 2025-05-19 15:26:27 +01:00
Cirx08
d40bfe99e1 Hide upload for reviewers but allow them to view the gallery list to delete wrongly approved images 2025-05-16 11:10:35 +01:00
Chris Collins
3cad6c4735 Merge branch 'prerel-item-owners' into 'main'
Prerel item owners

See merge request personal/wedding-share!61
2025-05-16 10:33:35 +01:00
Chris Collins
b236a0816f Prerel item owners 2025-05-16 10:33:35 +01:00
Chris Collins
fe727ed1b4 Merge branch 'prerel-sponsors-popup-tweaks' into 'main'
Updated sponsors popup

See merge request personal/wedding-share!60
2025-05-14 16:33:50 +01:00
Chris Collins
538e3cf5c7 Updated sponsors popup 2025-05-14 16:33:50 +01:00
Chris Collins
00548b99ff Merge branch 'prerel-gallery-download-fix' into 'main'
Prerel gallery download fix

See merge request personal/wedding-share!59
2025-05-13 15:59:00 +01:00
Chris Collins
4459cd400f Prerel gallery download fix 2025-05-13 15:58:59 +01:00
Cirx08
02bd858756 Added settings buttong to full width galleries 2025-05-10 23:03:57 +01:00
Cirx08
0d71453f8b Fixed Settings_Gallery_QRCodeIncludeCulture_Help translations 2025-05-10 10:36:30 +01:00
Chris Collins
ef67809f38 Merge branch 'prerel-ui-tweaks' into 'main'
Prerel ui tweaks

See merge request personal/wedding-share!58
2025-05-09 22:58:07 +01:00
Chris Collins
e9bdc5e334 Prerel ui tweaks 2025-05-09 22:58:07 +01:00
Cirx08
9332bb52e6 Added sponsor file 2025-05-09 22:22:32 +01:00
Chris Collins
4c5f0a00af Merge branch 'prerel-1.4.6.2' into 'main'
Added culture to QR codes

See merge request personal/wedding-share!57
2025-05-08 23:05:19 +01:00
Cirx08
849b623a04 Added culture to QR codes 2025-05-08 22:58:46 +01:00
Cirx08
b1194462cc Added bug fix for insecure qr code keys and review panel layout 2025-05-08 21:51:48 +01:00
Chris Collins
412d07c4ab Merge branch 'prerel-25-add-an-audit-log' into 'main'
Prerel 25 add an audit log

See merge request personal/wedding-share!56
2025-05-05 13:44:00 +01:00
Chris Collins
26b4a90870 Prerel 25 add an audit log 2025-05-05 13:44:00 +01:00
Chris Collins
a45dcc1b6b Merge branch 'prerel-26-gallery-settings-popup' into 'main'
Resolve "Gallery settings popup"

Closes #26

See merge request personal/wedding-share!55
2025-05-02 21:25:35 +01:00
Chris Collins
4b7dff9c79 Resolve "Gallery settings popup" 2025-05-02 21:25:35 +01:00
Chris Collins
8c9e530396 Merge branch 'prerel-21-upload-custom-resources' into 'main'
Prerel 21 upload custom resources

See merge request personal/wedding-share!54
2025-05-01 20:20:09 +01:00
Chris Collins
c23d08449f Prerel 21 upload custom resources 2025-05-01 20:20:09 +01:00
Chris Collins
f039112f27 Merge branch 'prerel-1.6.1' into 'main'
Prerel 1.6.1

See merge request personal/wedding-share!53
2025-04-28 22:22:59 +01:00
Chris Collins
ebba55cbea Prerel 1.6.1 2025-04-28 22:22:59 +01:00
Cirx08
d63ef87223 Prevent 2FA setup on demo mode 2025-04-28 19:48:10 +01:00
Cirx08
e60e4a9e05 Redditors are assholes 2025-04-25 21:50:07 +01:00
Chris Collins
4fe5f87c43 Merge branch 'prerel-22-ui-settings-page' into 'main'
Prerel 22 ui settings page

See merge request personal/wedding-share!52
2025-04-25 20:43:48 +01:00
Chris Collins
6ca1b92b0b Prerel 22 ui settings page 2025-04-25 20:43:48 +01:00
Chris Collins
6a085ba387 Merge branch 'cherry-pick-87c9fece' into 'main'
Fix slideshow full screen glitch

See merge request personal/wedding-share!51
2025-04-22 21:21:02 +01:00
Chris Collins
3c55c84f8d Fix slideshow full screen glitch 2025-04-22 21:21:02 +01:00
Chris Collins
7fa0d97187 Merge branch 'prerel-1.5.11.3' into 'main'
Bug fix for slideshow update on full width gallery

See merge request personal/wedding-share!50
2025-04-22 19:43:33 +01:00
Cirx08
a592d42db1 Bug fix for slideshow update on full width gallery 2025-04-22 19:41:00 +01:00
Chris Collins
6f4eefe600 Merge branch 'prerel-1.5.11.2' into 'main'
Fixed export bug and custom resources linking

See merge request personal/wedding-share!49
2025-04-20 13:36:01 +01:00
Cirx08
6f667d96c2 Fixed export bug and custom resources linking 2025-04-20 13:30:37 +01:00
Chris Collins
8555aa5263 Merge branch 'prerel-1.5.12' into 'main'
Bug fix for slideshow

See merge request personal/wedding-share!48
2025-04-19 10:48:36 +00:00
Cirx08
5ae8050b55 Bug fix for slideshow 2025-04-19 11:32:40 +01:00
Chris Collins
4e2308e9b5 Merge branch 'prerel-11-track-file-and-gallery-size-in-db' into 'main'
Prerel 11 track file and gallery size in db

See merge request personal/wedding-share!47
2025-04-15 18:09:02 +00:00
Chris Collins
cc71b347fb Prerel 11 track file and gallery size in db 2025-04-15 18:09:02 +00:00
Cirx08
8478627095 Added option to hide default gallery in selector 2025-04-04 21:36:30 +01:00
Chris Collins
a4715789a2 Merge branch 'prerel-1.5.10' into 'main'
Prerel 1.5.10

See merge request personal/wedding-share!46
2025-04-04 15:52:26 +00:00
Chris Collins
66403abc01 Prerel 1.5.10 2025-04-04 15:52:26 +00:00
Chris Collins
ad3e072c1c Merge branch 'prerel-1.5.9' into 'main'
Prerel 1.5.9

See merge request personal/wedding-share!45
2025-03-25 19:27:33 +00:00
Chris Collins
30c8f83c9f Prerel 1.5.9 2025-03-25 19:27:33 +00:00
Chris Collins
3c6a4f722e Updated translations for Ukraine 2025-03-25 17:37:32 +00:00
Chris Collins
0f137755b5 Merge branch 'prerel-1.5.8' into 'main'
Prerel 1.5.8

See merge request personal/wedding-share!44
2025-03-21 09:14:30 +00:00
Chris Collins
1c9bc7fd5f Prerel 1.5.8 2025-03-21 09:14:30 +00:00
Chris
d632cb75a7 Bug fix for password generation 2025-03-06 16:09:14 +00:00
Chris Collins
68ab7f1696 Merge branch 'prerel-1.5.7.2' into 'main'
Prerel 1.5.7.2

See merge request personal/wedding-share!43
2025-03-06 14:12:27 +00:00
Chris Collins
c9380ec484 Prerel 1.5.7.2 2025-03-06 14:12:27 +00:00
Chris Collins
b8cb4a0116 Merge branch 'prerel-1.5.7.1' into 'main'
Bug fix for logo image

See merge request personal/wedding-share!42
2025-03-05 09:44:19 +00:00
Chris
f543365ab2 Bug fix for logo image 2025-03-05 09:42:23 +00:00
Chris Collins
eb77dee91c Merge branch 'prerel-1.5.7' into 'main'
Prerel 1.5.7

See merge request personal/wedding-share!41
2025-03-02 15:15:31 +00:00
Chris Collins
23f823d96f Prerel 1.5.7 2025-03-02 15:15:31 +00:00
Chris Collins
ae4a1abdbe Merge branch 'prerel-1.5.6' into 'main'
v1.5.6

See merge request personal/wedding-share!40
2025-03-02 11:21:04 +00:00
Chris Collins
c998435312 v1.5.6 2025-03-02 11:21:04 +00:00
Chris Collins
9d58df9b89 Fix for email regex 2025-03-01 23:50:03 +00:00
Cirx08
4f0b299923 Updated readme 2025-03-01 12:58:36 +00:00
Chris Collins
8719820f69 Merge branch 'prerel-1.5.5' into 'main'
Prerel 1.5.5

See merge request personal/wedding-share!39
2025-03-01 12:52:57 +00:00
Chris Collins
2e103cbdee Prerel 1.5.5 2025-03-01 12:52:56 +00:00
Cirx08
8443f59c4e Fixed translation issue 2025-02-26 20:49:28 +00:00
Cirx08
79ff66093f Added translation request ticket type 2025-02-23 19:05:45 +00:00
Cirx08
1190798093 Updated German translations 2025-02-23 16:31:33 +00:00
Chris
3a245b695b Bug fix for loader not at top of screen 2025-02-21 12:41:07 +00:00
Chris
2c529ae117 Remove .ToLower() from lang selector 2025-02-21 12:17:09 +00:00
Chris Collins
1396e85a3e Merge branch 'prerel-1.5.4' into 'main'
Prerel 1.5.4

See merge request personal/wedding-share!38
2025-02-21 12:03:19 +00:00
Chris Collins
fa8cc19750 Prerel 1.5.4 2025-02-21 12:03:18 +00:00
Chris Collins
d5f0a7601f Merge branch 'prerel-1.5.3.1' into 'main'
Prerel 1.5.3.1

See merge request personal/wedding-share!37
2025-02-19 14:20:57 +00:00
Chris Collins
f96b2c827c Prerel 1.5.3.1 2025-02-19 14:20:57 +00:00
Chris Collins
15da9dbc08 Merge branch 'prerel-1.5.3' into 'main'
Prerel 1.5.3

See merge request personal/wedding-share!36
2025-02-18 13:32:51 +00:00
Chris Collins
10253c7021 Prerel 1.5.3 2025-02-18 13:32:51 +00:00
730 changed files with 332660 additions and 6209 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
github: [cirx08]
buy_me_a_coffee: cirx08

View File

@@ -0,0 +1,14 @@
---
name: Translation Request
about: Create a report to help us improve
title: "[Translations] - "
labels: translations
assignees: ''
---
**Description:**
A clear and concise description of the changes being made.
**Pull Request:**
A link to the pull request containing the updates to BOTH languages resource as specified by the documentation - `https://docs.wedding-share.org/docs/languages#addingupdating-languages`.

7
.gitignore vendored
View File

@@ -1,4 +1,4 @@
################################################################################
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
@@ -14,6 +14,7 @@
/WeddingShare/wwwroot/uploads/*
!/WeddingShare/wwwroot/uploads/default
/WeddingShare/wwwroot/uploads/default/*
/WeddingShare/wwwroot/custom_resources/*
/release notes
/WeddingShare/weddingshare.db
/WeddingShare/wedding-share.db
@@ -21,3 +22,7 @@
/WeddingShare/wwwroot/thumbnails
/WeddingShare/wwwroot/temp
/WeddingShare/ffmpeg
/WeddingShare/wwwroot/logos
/WeddingShare/wwwroot/banners
/WeddingShare/wwwroot/images/custom_resources
/WeddingShare/config/wedding-share.db.bak

View File

@@ -38,7 +38,7 @@ push_pre_release:
GIT_STRATEGY: none
stage: push
only:
- /^(prerel|rc|release)-[0-9]+\.[0-9]+\.[0-9]+/
- /^(prerel|rc|release)-.+/
script:
- 'docker buildx build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH --tag $CI_REGISTRY_IMAGE:pre_release --platform $BUILD_PLATFORMS --builder $BUILD_BUILDER_NAME --push -f $BUILD_DOCKERFILE .'
needs:

View File

@@ -42,7 +42,8 @@ namespace WeddingShare.UnitTests.Helpers
Title = $"{Guid.NewGuid()}.{MockFileExtension()}",
UploadedBy = rand.Next(2) % 2 == 0 ? Guid.NewGuid().ToString() : null,
MediaType = (MediaType)rand.Next(3),
State = state == GalleryItemState.All ? (GalleryItemState)rand.Next(2) : state
State = state == GalleryItemState.All ? (GalleryItemState)rand.Next(2) : state,
FileSize = (int)rand.Next(2),
};
}

View File

@@ -1,13 +1,12 @@
using System.Data.Common;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using System.Net;
using System.Text.Json;
using WeddingShare.Constants;
using WeddingShare.Controllers;
using WeddingShare.Enums;
using WeddingShare.Helpers;
@@ -22,8 +21,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
public class GalleryControllerTests
{
private readonly IWebHostEnvironment _env = Substitute.For<IWebHostEnvironment>();
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly IGalleryHelper _gallery = Substitute.For<IGalleryHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
private readonly IFileHelper _file = Substitute.For<IFileHelper>();
private readonly IDeviceDetector _deviceDetector = Substitute.For<IDeviceDetector>();
@@ -43,25 +41,20 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
_env.WebRootPath.Returns("/app/wwwroot");
_database.GetGallery("default").Returns(Task.FromResult<GalleryModel?>(new GalleryModel()
{
Id = 1,
Name = "default",
SecretKey = "password",
ApprovedItems = 32,
PendingItems = 50,
TotalItems = 72
}));
_database.GetGallery("blaa").Returns(Task.FromResult<GalleryModel?>(new GalleryModel()
{
Id = 2,
Name = "blaa",
SecretKey = "456789",
ApprovedItems = 2,
PendingItems = 1,
TotalItems = 3
}));
_database.GetGallery("missing").Returns(Task.FromResult<GalleryModel?>(null));
var mockData = GetMockData();
_database.GetGallery(1).Returns(Task.FromResult<GalleryModel?>(mockData["default"]));
_database.GetGallery(2).Returns(Task.FromResult<GalleryModel?>(mockData["blaa"]));
_database.GetGallery(3).Returns(Task.FromResult<GalleryModel?>(null));
_database.GetGalleryId("default").Returns(Task.FromResult<int?>(mockData["default"].Id));
_database.GetGalleryId("blaa").Returns(Task.FromResult<int?>(mockData["blaa"].Id));
_database.GetGalleryId("missing").Returns(Task.FromResult<int?>(null));
_database.GetGalleryIdByName("default").Returns(Task.FromResult<int?>(mockData["default"].Id));
_database.GetGalleryIdByName("blaa").Returns(Task.FromResult<int?>(mockData["blaa"].Id));
_database.GetGalleryIdByName("missing").Returns(Task.FromResult<int?>(null));
_database.AddGallery(Arg.Any<GalleryModel>()).Returns(Task.FromResult<GalleryModel?>(new GalleryModel()
{
Id = 101,
@@ -69,26 +62,27 @@ namespace WeddingShare.UnitTests.Tests.Helpers
SecretKey = "123456",
ApprovedItems = 0,
PendingItems = 0,
TotalItems = 0
TotalItems = 0,
Owner = 0
}));
_database.AddGalleryItem(Arg.Any<GalleryItemModel>()).Returns(Task.FromResult<GalleryItemModel?>(MockData.MockGalleryItem()));
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.All, Arg.Any<MediaType>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.All)));
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.Pending, Arg.Any<MediaType>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.Pending)));
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.Approved, Arg.Any<MediaType>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.Approved)));
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.All, Arg.Any<MediaType>(), Arg.Any<ImageOrientation>(), Arg.Any<GalleryGroup>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.All)));
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.Pending, Arg.Any<MediaType>(), Arg.Any<ImageOrientation>(), Arg.Any<GalleryGroup>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.Pending)));
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.Approved, Arg.Any<MediaType>(), Arg.Any<ImageOrientation>(), Arg.Any<GalleryGroup>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.Approved)));
_database.GetGalleryItemByChecksum(Arg.Any<int>(), Arg.Any<string>()).ReturnsNull();
_gallery.GetSecretKey(Arg.Any<string>()).Returns("password");
_gallery.GetSecretKey("blaa").Returns("456789");
_gallery.GetSecretKey("missing").Returns("123456");
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Upload", Arg.Any<bool>()).Returns(true);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Download", Arg.Any<bool>()).Returns(true);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Upload_Period", Arg.Any<string>()).Returns("1970-01-01 00:00:00");
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Prevent_Duplicates", Arg.Any<bool>()).Returns(true);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Default_View", Arg.Any<int>()).Returns((int)ViewMode.Default);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Allowed_File_Types", Arg.Any<string>()).Returns(".jpg,.jpeg,.png,.mp4,.mov");
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Require_Review", Arg.Any<bool>()).Returns(true);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Max_File_Size_Mb", Arg.Any<int>()).Returns(10);
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), Arg.Any<int>()).Returns("password");
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), 2).Returns("456789");
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), 101).Returns("123456");
_settings.GetOrDefault(Settings.Gallery.Upload, Arg.Any<bool>(), Arg.Any<int>()).Returns(true);
_settings.GetOrDefault(Settings.Gallery.Download, Arg.Any<bool>(), Arg.Any<int>()).Returns(true);
_settings.GetOrDefault(Settings.Gallery.UploadPeriod, Arg.Any<string>(), Arg.Any<int>()).Returns("1970-01-01 00:00:00");
_settings.GetOrDefault(Settings.Gallery.PreventDuplicates, Arg.Any<bool>(), Arg.Any<int>()).Returns(true);
_settings.GetOrDefault(Settings.Gallery.DefaultView, Arg.Any<int>(), Arg.Any<int>()).Returns((int)ViewMode.Default);
_settings.GetOrDefault(Settings.Gallery.AllowedFileTypes, Arg.Any<string>(), Arg.Any<int>()).Returns(".jpg,.jpeg,.png,.mp4,.mov");
_settings.GetOrDefault(Settings.Gallery.RequireReview, Arg.Any<bool>(), Arg.Any<int>()).Returns(true);
_settings.GetOrDefault(Settings.Gallery.MaxFileSizeMB, Arg.Any<int>(), Arg.Any<int>()).Returns(10);
_file.GetChecksum(Arg.Any<string>()).Returns(Guid.NewGuid().ToString());
@@ -97,47 +91,75 @@ namespace WeddingShare.UnitTests.Tests.Helpers
_localizer[Arg.Any<string>()].Returns(new LocalizedString("UnitTest", "UnitTest"));
}
[TestCase(DeviceType.Desktop, 1, "default", "password", ViewMode.Default, GalleryOrder.None)]
[TestCase(DeviceType.Mobile, 2, "blaa", "456789", ViewMode.Presentation, GalleryOrder.UploadedAsc)]
[TestCase(DeviceType.Tablet, 101, "missing", "123456", ViewMode.Slideshow, GalleryOrder.NameAsc)]
public async Task GalleryController_Index(DeviceType deviceType, int id, string name, string? key, ViewMode? mode, GalleryOrder order)
[TestCase(DeviceType.Desktop, 1, "default", "default", "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending, true)]
[TestCase(DeviceType.Mobile, 2, "blaa", "blaa", "456789", ViewMode.Presentation, GalleryGroup.Date, GalleryOrder.Ascending, true)]
[TestCase(DeviceType.Tablet, 101, "missing", "missing", "123456", ViewMode.Slideshow, GalleryGroup.Uploader, GalleryOrder.Ascending, false)]
public async Task GalleryController_Index(DeviceType deviceType, int id, string? identifier, string? name, string? key, ViewMode? mode, GalleryGroup group, GalleryOrder order, bool existing)
{
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(deviceType);
_config.GetOrDefault("Settings:Single_Gallery_Mode", Arg.Any<bool>()).Returns(false);
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
_settings.GetOrDefault(Settings.Basic.GuestGalleryCreation, Arg.Any<bool>()).Returns(false);
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
ViewResult actual = (ViewResult)await controller.Index(name, key, mode, order);
if (existing)
{
ViewResult actual = (ViewResult)await controller.Index(identifier, name, key, mode, group, order);
Assert.That(actual, Is.TypeOf<ViewResult>());
Assert.That(actual?.Model, Is.Not.Null);
PhotoGallery model = (PhotoGallery)actual.Model;
Assert.That(model?.Gallery?.Id, Is.EqualTo(id));
Assert.That(model?.Gallery?.Identifier, Is.EqualTo(identifier));
Assert.That(model?.Gallery?.Name, Is.EqualTo(name));
Assert.That(model?.SecretKey, Is.EqualTo(key));
Assert.That(model.ViewMode, Is.EqualTo(mode));
}
else
{
RedirectToActionResult actual = (RedirectToActionResult)await controller.Index(identifier, name, key, mode, group, order);
Assert.That(actual, Is.TypeOf<RedirectToActionResult>());
}
}
[TestCase(null, "default", "default")]
[TestCase("default", null, "default")]
[TestCase("default", "blaa", "blaa")]
public async Task GalleryController_Index_GetByIdentifier(string? id, string? identifier, string expected)
{
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(DeviceType.Desktop);
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
_settings.GetOrDefault(Settings.Basic.GuestGalleryCreation, Arg.Any<bool>()).Returns(false);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
ViewResult actual = (ViewResult)await controller.Index(id, identifier, "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Random);
Assert.That(actual, Is.TypeOf<ViewResult>());
Assert.That(actual?.Model, Is.Not.Null);
PhotoGallery model = (PhotoGallery)actual.Model;
Assert.That(model?.GalleryId, Is.EqualTo(id));
Assert.That(model?.GalleryName, Is.EqualTo(name));
Assert.That(model.ViewMode, Is.EqualTo(mode));
Assert.That(model?.FileUploader?.GalleryId, Is.EqualTo(name));
Assert.That(model?.FileUploader?.SecretKey, Is.EqualTo(key));
Assert.That(model?.FileUploader?.UploadUrl, Is.EqualTo("/Gallery/UploadImage"));
Assert.That(model?.Gallery?.Identifier, Is.EqualTo(expected));
}
[TestCase(true, true)]
[TestCase(false, false)]
public async Task GalleryController_UploadDisabled(bool disabled, bool expected)
public async Task GalleryController_UploadDisabled(bool enabled, bool expected)
{
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(DeviceType.Desktop);
_config.GetOrDefault("Settings:Single_Gallery_Mode", Arg.Any<bool>()).Returns(false);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Upload", Arg.Any<bool>()).Returns(disabled);
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
_settings.GetOrDefault(Settings.Gallery.Upload, Arg.Any<bool>(), Arg.Any<int>()).Returns(enabled);
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
ViewResult actual = (ViewResult)await controller.Index("default", "password", ViewMode.Default, GalleryOrder.None);
ViewResult actual = (ViewResult)await controller.Index("default", "default", "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending);
Assert.That(actual, Is.TypeOf<ViewResult>());
Assert.That(actual?.Model, Is.Not.Null);
PhotoGallery model = (PhotoGallery)actual.Model;
Assert.That(model?.FileUploader, expected ? Is.Not.Null : Is.Null);
Assert.That(model?.UploadActivated, Is.EqualTo(expected));
}
[TestCase("1970-01-01 00:00", true)]
@@ -148,42 +170,41 @@ namespace WeddingShare.UnitTests.Tests.Helpers
public async Task GalleryController_UploadDisabled(string uploadPeriod, bool expected)
{
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(DeviceType.Desktop);
_config.GetOrDefault("Settings:Single_Gallery_Mode", Arg.Any<bool>()).Returns(false);
_gallery.GetConfig(Arg.Any<string>(), "Gallery:Upload_Period", Arg.Any<string>()).Returns(uploadPeriod);
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
_settings.GetOrDefault(Settings.Gallery.UploadPeriod, Arg.Any<string>(), Arg.Any<int>()).Returns(uploadPeriod);
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
ViewResult actual = (ViewResult)await controller.Index("default", "password", ViewMode.Default, GalleryOrder.None);
ViewResult actual = (ViewResult)await controller.Index("default", "default", "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending);
Assert.That(actual, Is.TypeOf<ViewResult>());
Assert.That(actual?.Model, Is.Not.Null);
PhotoGallery model = (PhotoGallery)actual.Model;
Assert.That(model?.FileUploader, expected ? Is.Not.Null : Is.Null);
Assert.That(model?.UploadActivated, Is.EqualTo(expected));
}
[TestCase(DeviceType.Desktop, ViewMode.Default, GalleryOrder.None)]
[TestCase(DeviceType.Mobile, ViewMode.Presentation, GalleryOrder.UploadedAsc)]
[TestCase(DeviceType.Tablet, ViewMode.Slideshow, GalleryOrder.NameAsc)]
public async Task GalleryController_Index_SingleGalleryMode(DeviceType deviceType, ViewMode? mode, GalleryOrder order)
[TestCase(DeviceType.Desktop, ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending)]
[TestCase(DeviceType.Mobile, ViewMode.Presentation, GalleryGroup.Date, GalleryOrder.Ascending)]
[TestCase(DeviceType.Tablet, ViewMode.Slideshow, GalleryGroup.Uploader, GalleryOrder.Ascending)]
public async Task GalleryController_Index_SingleGalleryMode(DeviceType deviceType, ViewMode? mode, GalleryGroup group, GalleryOrder order)
{
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(deviceType);
_config.GetOrDefault("Settings:Single_Gallery_Mode", Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(true);
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
ViewResult actual = (ViewResult)await controller.Index("default", "password", mode, order);
ViewResult actual = (ViewResult)await controller.Index("default", "default", "password", mode, group, order);
Assert.That(actual, Is.TypeOf<ViewResult>());
Assert.That(actual?.Model, Is.Not.Null);
PhotoGallery model = (PhotoGallery)actual.Model;
Assert.That(model?.GalleryId, Is.EqualTo(1));
Assert.That(model?.GalleryName, Is.EqualTo("default"));
Assert.That(model?.Gallery?.Id, Is.EqualTo(1));
Assert.That(model?.Gallery?.Identifier, Is.EqualTo("default"));
Assert.That(model?.Gallery?.Name, Is.EqualTo("default"));
Assert.That(model?.SecretKey, Is.EqualTo("password"));
Assert.That(model.ViewMode, Is.EqualTo(mode));
Assert.That(model?.FileUploader?.GalleryId, Is.EqualTo("default"));
Assert.That(model?.FileUploader?.SecretKey, Is.EqualTo("password"));
Assert.That(model?.FileUploader?.UploadUrl, Is.EqualTo("/Gallery/UploadImage"));
}
[TestCase(true, 1, null)]
@@ -192,7 +213,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(false, 3, "Unit Testing")]
public async Task GalleryController_UploadImage(bool requiresReview, int fileCount, string? uploadedBy)
{
_config.GetOrDefault("Settings:Gallery:Require_Review", Arg.Any<bool>()).Returns(requiresReview);
_settings.GetOrDefault(Settings.Gallery.RequireReview, Arg.Any<bool>()).Returns(requiresReview);
var files = new FormFileCollection();
for (var i = 0; i < fileCount; i++)
@@ -203,12 +224,12 @@ namespace WeddingShare.UnitTests.Tests.Helpers
var session = new MockSession();
session.Set(SessionKey.ViewerIdentity, uploadedBy ?? string.Empty);
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
session: session,
form: new Dictionary<string, StringValues>
{
{ "Id", "default" },
{ "Id", "1" },
{ "SecretKey", "password" }
},
files: files);
@@ -233,12 +254,12 @@ namespace WeddingShare.UnitTests.Tests.Helpers
var session = new MockSession();
session.Set(SessionKey.ViewerIdentity, string.Empty);
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
session: session,
form: new Dictionary<string, StringValues>
{
{ "Id", "default" },
{ "Id", "1" },
{ "SecretKey", "password" }
},
files: files);
@@ -255,7 +276,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("")]
public async Task GalleryController_UploadImage_InvalidGallery(string? id)
{
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
{
{ "Id", id }
@@ -273,10 +294,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("")]
public async Task GalleryController_UploadImage_InvalidSecretKey(string? key)
{
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
{
{ "Id", "default" },
{ "Id", "1" },
{ "SecretKey", key }
});
@@ -291,7 +312,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase()]
public async Task GalleryController_UploadImage_MissingGallery()
{
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
{
{ "Id", Guid.NewGuid().ToString() }
@@ -308,10 +329,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase()]
public async Task GalleryController_UploadImage_NoFiles()
{
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
{
{ "Id", "default" },
{ "Id", "1" },
{ "SecretKey", "password" }
});
@@ -326,11 +347,11 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase()]
public async Task GalleryController_UploadImage_FileTooBig()
{
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
form: new Dictionary<string, StringValues>
{
{ "Id", "default" },
{ "Id", "1" },
{ "SecretKey", "password" }
},
files: new FormFileCollection() {
@@ -348,11 +369,11 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase()]
public async Task GalleryController_UploadImage_InvalidFileType()
{
var controller = new GalleryController(_env, _config, _database, _file, _gallery, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
form: new Dictionary<string, StringValues>
{
{ "Id", "default" },
{ "Id", "1" },
{ "SecretKey", "password" }
},
files: new FormFileCollection() {
@@ -366,5 +387,49 @@ namespace WeddingShare.UnitTests.Tests.Helpers
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
}
private IDictionary<string, GalleryModel> GetMockData()
{
return new Dictionary<string, GalleryModel>()
{
{
"default", new GalleryModel()
{
Id = 1,
Identifier = "default",
Name = "default",
SecretKey = "password",
ApprovedItems = 32,
PendingItems = 50,
TotalItems = 72
}
},
{
"blaa", new GalleryModel()
{
Id = 2,
Identifier = "blaa",
Name = "blaa",
SecretKey = "456789",
ApprovedItems = 2,
PendingItems = 1,
TotalItems = 3
}
},
{
"missing", new GalleryModel()
{
Id = 101,
Identifier = "missing",
Name = "missing",
SecretKey = "123456",
ApprovedItems = 0,
PendingItems = 0,
TotalItems = 0,
Owner = 0
}
}
};
}
}
}

View File

@@ -1,19 +1,23 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using WeddingShare.Constants;
using WeddingShare.Controllers;
using WeddingShare.Enums;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.UnitTests.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class HomeControllerTests
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly IGalleryHelper _gallery = Substitute.For<IGalleryHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
private readonly IDeviceDetector _deviceDetector = Substitute.For<IDeviceDetector>();
private readonly IAuditHelper _audit = Substitute.For<IAuditHelper>();
private readonly ILogger<HomeController> _logger = Substitute.For<ILogger<HomeController>>();
private readonly IStringLocalizer<Lang.Translations> _localizer = Substitute.For<IStringLocalizer<Lang.Translations>>();
@@ -37,10 +41,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
public async Task HomeController_Index(DeviceType deviceType, bool singleGalleryMode, string secretKey, bool isRedirect)
{
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(deviceType);
_config.GetOrDefault("Settings:Single_Gallery_Mode", Arg.Any<bool>()).Returns(singleGalleryMode);
_gallery.GetSecretKey(Arg.Any<string>()).Returns(secretKey);
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(singleGalleryMode);
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), Arg.Any<int>()).Returns(secretKey);
var controller = new HomeController(_config, _gallery, _deviceDetector, _logger, _localizer);
var controller = new HomeController(_settings, _database, _deviceDetector, _audit, _logger, _localizer);
controller.ControllerContext.HttpContext = new DefaultHttpContext()
{
Session = new MockSession()
@@ -58,7 +62,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
Assert.That(actual.Permanent, Is.EqualTo(false));
Assert.That(actual.ControllerName, Is.EqualTo("Gallery"));
Assert.That(actual.ActionName, Is.EqualTo("Index"));
Assert.That(actual.RouteValues, Is.Null);
Assert.That(actual.RouteValues, singleGalleryMode ? Is.EqualTo(new RouteValueDictionary { { "identifier", "default" } }) : Is.Null);
Assert.That(actual.Fragment, Is.Null);
}
}

View File

@@ -0,0 +1,45 @@
using WeddingShare.Extensions;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class DictionaryExtensionsTests
{
private readonly IDictionary<string, string> _data;
public DictionaryExtensionsTests()
{
_data = new Dictionary<string, string>()
{
{ "KEY_1", "1" },
{ "KEY_2", "2" },
{ "KEY_3", "3" },
{ "KEY_4", "4" },
};
}
[SetUp]
public void Setup()
{
}
[TestCase("KEY_1", "1")]
[TestCase("KEY_2", "2")]
[TestCase("KEY_3", "3")]
[TestCase("KEY_99", "")]
public void DictionaryExtensions_GetValue(string key, string expected)
{
var actual = _data.GetValue(key);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("KEY_1", "Default", "1")]
[TestCase("KEY_2", "Default", "2")]
[TestCase("KEY_3", "Default", "3")]
[TestCase("KEY_99", "Default", "Default")]
public void DictionaryExtensions_GetValue_DefaultValue(string key, string defaultValue, string? expected)
{
var actual = _data.GetValue(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
}
}

View File

@@ -8,7 +8,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
public class EmailHelperTests
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
private readonly ISmtpClientWrapper _smtp = Substitute.For<ISmtpClientWrapper>();
private readonly ILogger<EmailHelper> _logger = Substitute.For<ILogger<EmailHelper>>();
@@ -21,21 +21,21 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
_smtp.SendMailAsync(Arg.Any<SmtpClient>(), Arg.Any<MailMessage>()).Returns(Task.FromResult(true));
_config.GetOrDefault("Notifications:Smtp:Enabled", Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Smtp:Recipient", Arg.Any<string>()).Returns("unit@test.com");
_config.GetOrDefault("Notifications:Smtp:Host", Arg.Any<string>()).Returns("https://unit.test.com/");
_config.GetOrDefault("Notifications:Smtp:Port", Arg.Any<int>()).Returns(999);
_config.GetOrDefault("Notifications:Smtp:Username", Arg.Any<string>()).Returns("Unit");
_config.GetOrDefault("Notifications:Smtp:Password", Arg.Any<string>()).Returns("Test");
_config.GetOrDefault("Notifications:Smtp:From", Arg.Any<string>()).Returns("unittest@test.com");
_config.GetOrDefault("Notifications:Smtp:DisplayName", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Smtp:UseSSL", Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, Arg.Any<string>()).Returns("unit@test.com");
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns("https://unit.test.com/");
_settings.GetOrDefault(Constants.Notifications.Smtp.Port, Arg.Any<int>()).Returns(999);
_settings.GetOrDefault(Constants.Notifications.Smtp.Username, Arg.Any<string>()).Returns("Unit");
_settings.GetOrDefault(Constants.Notifications.Smtp.Password, Arg.Any<string>()).Returns("Test");
_settings.GetOrDefault(Constants.Notifications.Smtp.From, Arg.Any<string>()).Returns("unittest@test.com");
_settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Smtp.UseSSL, Arg.Any<bool>()).Returns(true);
}
[TestCase("unit", "test")]
public async Task EmailHelper_Success(string title, string message)
{
var actual = await new EmailHelper(_config, _smtp, _logger).Send(title, message);
var actual = await new EmailHelper(_settings, _smtp, _logger).Send(title, message);
Assert.That(actual, Is.EqualTo(true));
}
@@ -43,9 +43,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(false, false)]
public async Task EmailHelper_Enabled(bool enabled, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:Enabled", Arg.Any<bool>()).Returns(enabled);
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(enabled);
var actual = await new EmailHelper(_config, _smtp, _logger).Send("unit", "test");
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -54,9 +54,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("blaa@blaa.com", true)]
public async Task EmailHelper_Recipient(string recipient, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:Recipient", Arg.Any<string>()).Returns(recipient);
_settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, Arg.Any<string>()).Returns(recipient);
var actual = await new EmailHelper(_config, _smtp, _logger).Send("unit", "test");
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -65,9 +65,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("https://unit.test.com/", true)]
public async Task EmailHelper_Host(string host, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:Host", Arg.Any<string>()).Returns(host);
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns(host);
var actual = await new EmailHelper(_config, _smtp, _logger).Send("unit", "test");
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -77,9 +77,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(1, true)]
public async Task EmailHelper_Port(int port, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:Port", Arg.Any<int>()).Returns(port);
_settings.GetOrDefault(Constants.Notifications.Smtp.Port, Arg.Any<int>()).Returns(port);
var actual = await new EmailHelper(_config, _smtp, _logger).Send("unit", "test");
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -88,9 +88,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("blaa@blaa.com", true)]
public async Task EmailHelper_From(string from, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:From", Arg.Any<string>()).Returns(from);
_settings.GetOrDefault(Constants.Notifications.Smtp.From, Arg.Any<string>()).Returns(from);
var actual = await new EmailHelper(_config, _smtp, _logger).Send("unit", "test");
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -99,9 +99,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("UnitTest", true)]
public async Task EmailHelper_DisplayName(string displayName, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:DisplayName", Arg.Any<string>()).Returns(displayName);
_settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, Arg.Any<string>()).Returns(displayName);
var actual = await new EmailHelper(_config, _smtp, _logger).Send("unit", "test");
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
}

View File

@@ -1,15 +1,16 @@
using WeddingShare.Constants;
using WeddingShare.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class EncrytpionHelper
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
public EncrytpionHelper()
{
_config.GetOrDefault("Security:Encryption:HashType", Arg.Any<string>()).Returns("SHA256");
_config.GetOrDefault("Security:Encryption:Iterations", Arg.Any<int>()).Returns(1000);
_settings.GetOrDefault(Security.Encryption.HashType, Arg.Any<string>()).Returns("SHA256");
_settings.GetOrDefault(Security.Encryption.Iterations, Arg.Any<int>()).Returns(1000);
}
[SetUp]
@@ -22,10 +23,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("Test", "Key3", "Salt3", "47VYeotX2C8GPuhaQlrWXg==")]
public void EncrytpionHelper_ValidDetails(string value, string key, string salt, string expected)
{
_config.GetOrDefault("Security:Encryption:Key", Arg.Any<string>()).Returns(key);
_config.GetOrDefault("Security:Encryption:Salt", Arg.Any<string>()).Returns(salt);
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
var actual = new EncryptionHelper(_config).Encrypt(value);
var actual = new EncryptionHelper(_settings).Encrypt(value);
Assert.That(actual, Is.EqualTo(expected));
}
@@ -34,10 +35,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("Test3", "Salt3")]
public void EncrytpionHelper_NoKey(string value, string salt)
{
_config.GetOrDefault("Security:Encryption:Key", Arg.Any<string>()).Returns(string.Empty);
_config.GetOrDefault("Security:Encryption:Salt", Arg.Any<string>()).Returns(salt);
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(string.Empty);
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
var actual = new EncryptionHelper(_config).Encrypt(value);
var actual = new EncryptionHelper(_settings).Encrypt(value);
Assert.That(actual, Is.EqualTo(value));
}
@@ -46,10 +47,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("Test3", "Key3")]
public void EncrytpionHelper_NoSalt(string value, string key)
{
_config.GetOrDefault("Security:Encryption:Key", Arg.Any<string>()).Returns(key);
_config.GetOrDefault("Security:Encryption:Salt", Arg.Any<string>()).Returns(string.Empty);
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(string.Empty);
var actual = new EncryptionHelper(_config).Encrypt(value);
var actual = new EncryptionHelper(_settings).Encrypt(value);
Assert.That(actual, Is.EqualTo(value));
}
@@ -58,13 +59,13 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("Test3", "Key3", "Salt3")]
public void EncrytpionHelper_DifferentHashes(string value, string key, string salt)
{
_config.GetOrDefault("Security:Encryption:Key", Arg.Any<string>()).Returns(key);
_config.GetOrDefault("Security:Encryption:Salt", Arg.Any<string>()).Returns(salt);
var helper1 = new EncryptionHelper(_config).Encrypt(value);
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
var helper1 = new EncryptionHelper(_settings).Encrypt(value);
_config.GetOrDefault("Security:Encryption:Key", Arg.Any<string>()).Returns("Unit");
_config.GetOrDefault("Security:Encryption:Salt", Arg.Any<string>()).Returns("Test");
var helper2 = new EncryptionHelper(_config).Encrypt(value);
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns("Unit");
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns("Test");
var helper2 = new EncryptionHelper(_settings).Encrypt(value);
Assert.That(helper1, Is.Not.EqualTo(helper2));
}
@@ -74,10 +75,10 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("Key", "Salt", true)]
public void EncrytpionHelper_IsEncryptionEnabled(string key, string salt, bool expected)
{
_config.GetOrDefault("Security:Encryption:Key", Arg.Any<string>()).Returns(key);
_config.GetOrDefault("Security:Encryption:Salt", Arg.Any<string>()).Returns(salt);
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
var actual = new EncryptionHelper(_config).IsEncryptionEnabled();
var actual = new EncryptionHelper(_settings).IsEncryptionEnabled();
Assert.That(actual, Is.EqualTo(expected));
}

View File

@@ -0,0 +1,81 @@
using Microsoft.Extensions.Logging;
using WeddingShare.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class FileHelperTests
{
private readonly ILogger<FileHelper> _logger = Substitute.For<ILogger<FileHelper>>();
public FileHelperTests()
{
}
[SetUp]
public void Setup()
{
}
[TestCase(-1, "0 B")]
[TestCase(0, "0 B")]
[TestCase(1, "1 B")]
[TestCase(2, "2 B")]
[TestCase(3, "3 B")]
public void FileHelper_BytesToHumanReadable(long bytes, string expected)
{
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 0);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(-1, "0 B")]
[TestCase(0, "0 B")]
[TestCase(1, "1 B")]
[TestCase(10, "10 B")]
[TestCase(100, "100 B")]
[TestCase(1000, "1 KB")]
[TestCase(1000000, "1 MB")]
[TestCase(1000000000, "1 GB")]
[TestCase(1000000000000, "1 TB")]
[TestCase(1000000000000000, "1 PB")]
[TestCase(1000000000000000000, "1 EB")]
public void FileHelper_BytesToHumanReadable_No_Places(long bytes, string expected)
{
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 0);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(-1, "0.0 B")]
[TestCase(0, "0.0 B")]
[TestCase(1, "1.0 B")]
[TestCase(10, "10.0 B")]
[TestCase(100, "100.0 B")]
[TestCase(1000, "1.0 KB")]
[TestCase(1000000, "1.0 MB")]
[TestCase(1000000000, "1.0 GB")]
[TestCase(1000000000000, "1.0 TB")]
[TestCase(1000000000000000, "1.0 PB")]
[TestCase(1000000000000000000, "1.0 EB")]
public void FileHelper_BytesToHumanReadable_1_Places(long bytes, string expected)
{
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 1);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(-1, "0.00 B")]
[TestCase(0, "0.00 B")]
[TestCase(1, "1.00 B")]
[TestCase(10, "10.00 B")]
[TestCase(100, "100.00 B")]
[TestCase(1000, "1.00 KB")]
[TestCase(1000000, "1.00 MB")]
[TestCase(1000000000, "1.00 GB")]
[TestCase(1000000000000, "1.00 TB")]
[TestCase(1000000000000000, "1.00 PB")]
[TestCase(1000000000000000000, "1.00 EB")]
public void FileHelper_BytesToHumanReadable_2_Places(long bytes, string expected)
{
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 2);
Assert.That(actual, Is.EqualTo(expected));
}
}
}

View File

@@ -1,136 +0,0 @@
using Microsoft.Extensions.Logging;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.Models.Database;
using WeddingShare.UnitTests.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class GalleryHelperTests
{
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
public GalleryHelperTests()
{
_database.GetGallery("Gallery1").Returns(new GalleryModel() { SecretKey = "001" });
_database.GetGallery("Gallery2").Returns(new GalleryModel() { SecretKey = "002" });
}
[SetUp]
public void Setup()
{
}
[TestCase()]
public async Task GalleryHelper_GetSecretKey_DefaultEnvKey()
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable("GALLERY_SECRET_KEY").Returns("123");
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Gallery:Secret_Key_Gallery2", "002" }
});
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = await new GalleryHelper(config, _database).GetSecretKey("Gallery3");
Assert.That(actual, Is.EqualTo("123"));
}
[TestCase()]
public async Task GalleryHelper_GetSecretKey_GalleryEnvKey()
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable("GALLERY_SECRET_KEY").Returns("123");
environment.GetEnvironmentVariable("GALLERY_SECRET_KEY_GALLERY1").Returns("001");
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Gallery:Secret_Key_Gallery2", "002" }
});
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = await new GalleryHelper(config, _database).GetSecretKey("Gallery1");
Assert.That(actual, Is.EqualTo("001"));
}
[TestCase("Gallery1", "001")]
[TestCase("Gallery2", "002")]
[TestCase("Gallery3", null)]
public async Task GalleryHelper_GetSecretKey_Database(string galleryId, string key)
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable(Arg.Any<string>()).Returns(string.Empty);
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>());
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = await new GalleryHelper(config, _database).GetSecretKey(galleryId);
Assert.That(actual, Is.EqualTo(key));
}
[TestCase()]
public void GalleryHelper_GetConfig_DefaultEnvKey()
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable("GALLERY_SECRET_KEY").Returns("123");
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Gallery:Secret_Key_Gallery2", "002" }
});
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = new GalleryHelper(config, _database).GetConfig("Gallery3", "Gallery:Secret_Key");
Assert.That(actual, Is.EqualTo("123"));
}
[TestCase("Gallery:Secret_Key", "001")]
[TestCase("Gallery:Columns", "001")]
public void GalleryHelper_GetConfig_GalleryEnvKey(string key, string expected)
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable("SECRET_KEY").Returns("123");
environment.GetEnvironmentVariable("SECRET_KEY_GALLERY1").Returns("001");
environment.GetEnvironmentVariable("GALLERY_COLUMNS").Returns("123");
environment.GetEnvironmentVariable("GALLERY_COLUMNS_GALLERY1").Returns("001");
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Gallery:Secret_Key_Gallery2", "002" },
{ "Gallery:Columns_Gallery2", "002" }
});
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = new GalleryHelper(config, _database).GetConfig("Gallery1", key);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("Disable_QR_Code", "true")]
[TestCase("Gallery:QR_Code", "false")]
public void GalleryHelper_GetConfig_Migrated_GalleryEnvKey(string key, string expected)
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable("DISABLE_QR_CODE").Returns("false");
environment.GetEnvironmentVariable("DISABLE_QR_CODE_GALLERY1").Returns("true");
environment.GetEnvironmentVariable("GALLERY_QR_CODE").Returns("true");
environment.GetEnvironmentVariable("GALLERY_QR_CODE_GALLERY1").Returns("false");
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Disable_QR_Code_Gallery2", "false" },
{ "Gallery:QR_Code_Gallery2", "true" }
});
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = new GalleryHelper(config, _database).GetConfig("Gallery1", key);
Assert.That(actual, Is.EqualTo(expected));
}
}
}

View File

@@ -8,7 +8,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
public class GotifyHelperTests
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
private readonly IHttpClientFactory _clientFactory = Substitute.For<IHttpClientFactory>();
private readonly ILogger<GotifyHelper> _logger = Substitute.For<ILogger<GotifyHelper>>();
@@ -24,16 +24,16 @@ namespace WeddingShare.UnitTests.Tests.Helpers
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
_config.GetOrDefault("Notifications:Gotify:Enabled", Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Gotify:Endpoint", Arg.Any<string>()).Returns("https://unit.test.com/");
_config.GetOrDefault("Notifications:Gotify:Token", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Gotify:Priority", Arg.Any<int>()).Returns(4);
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
_settings.GetOrDefault(Constants.Notifications.Gotify.Token, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Gotify.Priority, Arg.Any<int>()).Returns(4);
}
[TestCase("unit", "test")]
public async Task GotifyHelper_Success(string title, string message)
{
var actual = await new GotifyHelper(_config, _clientFactory, _logger).Send(title, message);
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send(title, message);
Assert.That(actual, Is.EqualTo(true));
}
@@ -41,9 +41,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(false, false)]
public async Task GotifyHelper_Enabled(bool enabled, bool expected)
{
_config.GetOrDefault("Notifications:Gotify:Enabled", Arg.Any<bool>()).Returns(enabled);
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(enabled);
var actual = await new GotifyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -57,7 +57,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
var actual = await new GotifyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -66,9 +66,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("UnitTest", true)]
public async Task GotifyHelper_Token(string? token, bool expected)
{
_config.GetOrDefault("Notifications:Gotify:Token", Arg.Any<string>()).Returns(token);
_settings.GetOrDefault(Constants.Notifications.Gotify.Token, Arg.Any<string>()).Returns(token);
var actual = await new GotifyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -79,9 +79,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(100, true)]
public async Task GotifyHelper_Priority(int priority, bool expected)
{
_config.GetOrDefault("Notifications:Gotify:Priority", Arg.Any<int>()).Returns(priority);
_settings.GetOrDefault(Constants.Notifications.Gotify.Priority, Arg.Any<int>()).Returns(priority);
var actual = await new GotifyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
}

View File

@@ -0,0 +1,62 @@
using WeddingShare.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class HtmlSanitizerTests
{
public HtmlSanitizerTests()
{
}
[SetUp]
public void Setup()
{
}
[TestCase("<div><b>Test</b></div>", new[] { "b" }, "<div>Test</div>")]
[TestCase("<div><b>Test</b></div>", new[] { "div" }, "<b>Test</b>")]
[TestCase("<div><b>Test</b></div>", new[] { "div", "b" }, "Test")]
[TestCase("<script src=\"asdasd\">Test</script>", new[] { "script" }, "Test")]
[TestCase("<script src=\"asdasd\"/>", new[] { "script" }, "")]
[TestCase("<img src=\"asdasd\"/>", new[] { "img" }, "")]
[TestCase("Blaa<div><b>Test</b></div>", new[] { ".*" }, "Blaa")]
public void HtmlSanitizer_SanitizeHtmlTags(string input, string[] tags, string expected)
{
var actual = HtmlSanitizer.SanitizeHtmlTags(input, tags);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("<script src=\"blaa\" link=\"unit\"></script>", new[] { "src" }, "<script link=\"unit\"></script>")]
[TestCase("<script src=\"blaa\" link=\"unit\"></script>", new[] { "link" }, "<script src=\"blaa\" ></script>")]
[TestCase("<script src=\"blaa\" link=\"unit\"></script>", new[] { ".*" }, "></script>")]
public void HtmlSanitizer_SanitizeHtmlAttributes(string input, string[] tags, string expected)
{
var actual = HtmlSanitizer.SanitizeHtmlAttributes(input, tags);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("this is a https://unit.com/ test", "this is a test")]
[TestCase("this is a https://www.unit.com/ test", "this is a test")]
[TestCase("this is a https://www.unit.com/home test", "this is a test")]
[TestCase("this is a http://unit.com/ test", "this is a test")]
[TestCase("this is a http://www.unit.com/ test", "this is a test")]
[TestCase("this is a http://www.unit.com/home test", "this is a test")]
public void HtmlSanitizer_SanitizeHtmlAttributes(string input, string expected)
{
var actual = HtmlSanitizer.SanitizeLinks(input);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("this is a test", false)]
[TestCase("this is a <img src=x onerror=alert(0)> test", true)]
[TestCase("this is a <script/> test", true)]
[TestCase("this is a <script></script> test", true)]
[TestCase("this is a http://www.unit.com/home test", true)]
[TestCase("this is a https://www.unit.com/home test", true)]
public void HtmlSanitizer_MayContainXss(string input, bool expected)
{
var actual = HtmlSanitizer.MayContainXss(input);
Assert.That(actual, Is.EqualTo(expected));
}
}
}

View File

@@ -0,0 +1,50 @@
using System.Globalization;
using WeddingShare.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class LanguageHelperTests
{
private readonly List<CultureInfo> _supportedCultures;
public LanguageHelperTests()
{
_supportedCultures = new List<CultureInfo>()
{
new CultureInfo("en-GB"),
new CultureInfo("fr-FR"),
new CultureInfo("de-DE"),
};
}
[SetUp]
public void Setup()
{
}
[TestCase("en-GB", true)]
[TestCase("fr-FR", true)]
[TestCase("de-DE", true)]
[TestCase("de-de", true)]
[TestCase("en-US", false)]
[TestCase("cn-CSS", false)]
[TestCase("", false)]
public void LanguageHelper_IsCultureSupported(string culture, bool expected)
{
var actual = new LanguageHelper().IsCultureSupported(culture, _supportedCultures);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("en-GB", "en-GB", "en-GB")]
[TestCase("en-US", "en-GB", "en-GB")]
[TestCase("en-US", "en-US", "en-GB")]
[TestCase("fr-FR", "en-GB", "fr-FR")]
[TestCase("de-DE", "fr-FR", "de-DE")]
[TestCase("de-de", "fr-fr", "de-DE")]
public void LanguageHelper_GetOrFallbackCulture(string culture, string fallback, string expected)
{
var actual = new LanguageHelper().GetOrFallbackCulture(culture, fallback, _supportedCultures);
Assert.That(actual, Is.EqualTo(expected));
}
}
}

View File

@@ -9,7 +9,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
public class NotificationBrokerTests
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
private readonly IHttpClientFactory _clientFactory = Substitute.For<IHttpClientFactory>();
private readonly ISmtpClientWrapper _smtp = Substitute.For<ISmtpClientWrapper>();
private readonly ILoggerFactory _logger = Substitute.For<ILoggerFactory>();
@@ -28,26 +28,26 @@ namespace WeddingShare.UnitTests.Tests.Helpers
_smtp.SendMailAsync(Arg.Any<SmtpClient>(), Arg.Any<MailMessage>()).Returns(Task.FromResult(true));
_config.GetOrDefault("Notifications:Smtp:Enabled", Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Smtp:Recipient", Arg.Any<string>()).Returns("unit@test.com");
_config.GetOrDefault("Notifications:Smtp:Host", Arg.Any<string>()).Returns("https://unit.test.com/");
_config.GetOrDefault("Notifications:Smtp:Port", Arg.Any<int>()).Returns(999);
_config.GetOrDefault("Notifications:Smtp:Username", Arg.Any<string>()).Returns("Unit");
_config.GetOrDefault("Notifications:Smtp:Password", Arg.Any<string>()).Returns("Test");
_config.GetOrDefault("Notifications:Smtp:From", Arg.Any<string>()).Returns("unittest@test.com");
_config.GetOrDefault("Notifications:Smtp:DisplayName", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Smtp:UseSSL", Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, Arg.Any<string>()).Returns("unit@test.com");
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns("https://unit.test.com/");
_settings.GetOrDefault(Constants.Notifications.Smtp.Port, Arg.Any<int>()).Returns(999);
_settings.GetOrDefault(Constants.Notifications.Smtp.Username, Arg.Any<string>()).Returns("Unit");
_settings.GetOrDefault(Constants.Notifications.Smtp.Password, Arg.Any<string>()).Returns("Test");
_settings.GetOrDefault(Constants.Notifications.Smtp.From, Arg.Any<string>()).Returns("unittest@test.com");
_settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Smtp.UseSSL, Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Ntfy:Enabled", Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Ntfy:Endpoint", Arg.Any<string>()).Returns("https://unit.test.com/");
_config.GetOrDefault("Notifications:Ntfy:Token", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Ntfy:Topic", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Ntfy:Priority", Arg.Any<int>()).Returns(4);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
_settings.GetOrDefault(Constants.Notifications.Ntfy.Token, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, Arg.Any<int>()).Returns(4);
_config.GetOrDefault("Notifications:Gotify:Enabled", Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Gotify:Endpoint", Arg.Any<string>()).Returns("https://unit.test.com/");
_config.GetOrDefault("Notifications:Gotify:Token", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Gotify:Priority", Arg.Any<int>()).Returns(4);
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
_settings.GetOrDefault(Constants.Notifications.Gotify.Token, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Gotify.Priority, Arg.Any<int>()).Returns(4);
}
[TestCase(false, false, false, true)]
@@ -57,38 +57,38 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(true, true, true, true)]
public async Task NotificationBroker_Success(bool smtp, bool ntfy, bool gotify, bool expected)
{
_config.GetOrDefault("Notifications:Smtp:Enabled", Arg.Any<bool>()).Returns(smtp);
_config.GetOrDefault("Notifications:Ntfy:Enabled", Arg.Any<bool>()).Returns(ntfy);
_config.GetOrDefault("Notifications:Gotify:Enabled", Arg.Any<bool>()).Returns(gotify);
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(smtp);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(ntfy);
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(gotify);
var actual = await new NotificationBroker(_config, _smtp, _clientFactory, _logger).Send("unit", "test");
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase()]
public async Task NotificationBroker_Issue_Smtp()
{
_config.GetOrDefault("Notifications:Smtp:Host", Arg.Any<string>()).Returns(string.Empty);
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns(string.Empty);
var actual = await new NotificationBroker(_config, _smtp, _clientFactory, _logger).Send("unit", "test");
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(false));
}
[TestCase()]
public async Task NotificationBroker_Issue_Ntfy()
{
_config.GetOrDefault("Notifications:Ntfy:Endpoint", Arg.Any<string>()).Returns(string.Empty);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, Arg.Any<string>()).Returns(string.Empty);
var actual = await new NotificationBroker(_config, _smtp, _clientFactory, _logger).Send("unit", "test");
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(false));
}
[TestCase()]
public async Task NotificationBroker_Issue_Gotify()
{
_config.GetOrDefault("Notifications:Gotify:Endpoint", Arg.Any<string>()).Returns(string.Empty);
_settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, Arg.Any<string>()).Returns(string.Empty);
var actual = await new NotificationBroker(_config, _smtp, _clientFactory, _logger).Send("unit", "test");
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(false));
}
}

View File

@@ -9,7 +9,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
public class NtfyHelperTests
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
private readonly IHttpClientFactory _clientFactory = Substitute.For<IHttpClientFactory>();
private readonly ILogger<NtfyHelper> _logger = Substitute.For<ILogger<NtfyHelper>>();
@@ -25,17 +25,17 @@ namespace WeddingShare.UnitTests.Tests.Helpers
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
_config.GetOrDefault("Notifications:Ntfy:Enabled", Arg.Any<bool>()).Returns(true);
_config.GetOrDefault("Notifications:Ntfy:Endpoint", Arg.Any<string>()).Returns("https://unit.test.com/");
_config.GetOrDefault("Notifications:Ntfy:Token", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Ntfy:Topic", Arg.Any<string>()).Returns("UnitTest");
_config.GetOrDefault("Notifications:Ntfy:Priority", Arg.Any<int>()).Returns(4);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(true);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
_settings.GetOrDefault(Constants.Notifications.Ntfy.Token, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, Arg.Any<string>()).Returns("UnitTest");
_settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, Arg.Any<int>()).Returns(4);
}
[TestCase("unit", "test")]
public async Task NtfyHelper_Success(string title, string message)
{
var actual = await new NtfyHelper(_config, _clientFactory, _logger).Send(title, message);
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send(title, message);
Assert.That(actual, Is.EqualTo(true));
}
@@ -43,9 +43,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(false, false)]
public async Task NtfyHelper_Enabled(bool enabled, bool expected)
{
_config.GetOrDefault("Notifications:Ntfy:Enabled", Arg.Any<bool>()).Returns(enabled);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(enabled);
var actual = await new NtfyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -59,18 +59,18 @@ namespace WeddingShare.UnitTests.Tests.Helpers
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
var actual = await new NtfyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase(null, false)]
[TestCase("", false)]
[TestCase(null, true)]
[TestCase("", true)]
[TestCase("UnitTest", true)]
public async Task NtfyHelper_Token(string? token, bool expected)
{
_config.GetOrDefault("Notifications:Ntfy:Token", Arg.Any<string>()).Returns(token);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Token, Arg.Any<string>()).Returns(token);
var actual = await new NtfyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -79,9 +79,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("UnitTest", true)]
public async Task NtfyHelper_Topic(string? topic, bool expected)
{
_config.GetOrDefault("Notifications:Ntfy:Topic", Arg.Any<string>()).Returns(topic);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, Arg.Any<string>()).Returns(topic);
var actual = await new NtfyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
@@ -92,9 +92,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase(100, true)]
public async Task NtfyHelper_Priority(int priority, bool expected)
{
_config.GetOrDefault("Notifications:Ntfy:Priority", Arg.Any<int>()).Returns(priority);
_settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, Arg.Any<int>()).Returns(priority);
var actual = await new NtfyHelper(_config, _clientFactory, _logger).Send("unit", "test");
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
Assert.That(actual, Is.EqualTo(expected));
}
}

View File

@@ -0,0 +1,195 @@
using Microsoft.Extensions.Logging;
using System;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.Models.Database;
using WeddingShare.UnitTests.Helpers;
namespace WeddingShare.UnitTests.Tests.Helpers
{
public class SettingsHelperTests
{
private readonly IConfigHelper _config;
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
private readonly ILogger<SettingsHelper> _logger = Substitute.For<ILogger<SettingsHelper>>();
public SettingsHelperTests()
{
var environment = Substitute.For<IEnvironmentWrapper>();
environment.GetEnvironmentVariable("VERSION").Returns("v2.0.0");
environment.GetEnvironmentVariable("ENVKEY_1").Returns("EnvValue1");
environment.GetEnvironmentVariable("ENVKEY_2").Returns("EnvValue2");
environment.GetEnvironmentVariable("ENVKEY_3").Returns("EnvValue3");
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Release:Version", "v1.0.0" },
{ "Release:Plugin:Version", "v3.0.0" },
{ "String1:Key1", "Value1" },
{ "String1:Key2", "Value2" },
{ "String2:Key1", "Value3" },
{ "Int1:Key1", "1" },
{ "Int1:Key2", "2" },
{ "Int2:Key1", "3" },
{ "Long1:Key1", "4" },
{ "Long1:Key2", "5" },
{ "Long2:Key1", "6" },
{ "Decimal1:Key1", "4.12" },
{ "Decimal1:Key2", "5.45" },
{ "Decimal2:Key1", "6.733" },
{ "Double1:Key1", "4.12" },
{ "Double1:Key2", "5.45" },
{ "Double2:Key1", "6.733" },
{ "Boolean1:Key1", "true" },
{ "Boolean1:Key2", "false" },
{ "Boolean2:Key1", "true" },
{ "DateTime1:Key1", "1987-11-20 08:00:00" },
{ "DateTime1:Key2", "2000-08-12 12:00:00" },
{ "DateTime2:Key1", "2018-01-01 20:30:10" },
});
_config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
_database.GetSetting("Setting1").Returns(new SettingModel() { Value = "001" });
_database.GetSetting("Setting2").Returns(new SettingModel() { Value = "002" });
_database.GetSetting("Version").Returns(new SettingModel() { Value = "v4.0.0" });
_database.AddSetting(Arg.Any<SettingModel>()).Returns(new SettingModel() { Value = "Added" });
_database.EditSetting(Arg.Any<SettingModel>()).Returns(new SettingModel() { Value = "Updated" });
_database.SetSetting(Arg.Any<SettingModel>()).Returns(new SettingModel() { Value = "Set" });
_database.DeleteSetting(Arg.Any<SettingModel>()).Returns(true);
}
[SetUp]
public void Setup()
{
}
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("Setting1", "001")]
[TestCase("Setting2", "002")]
public async Task SettingsHelper_GetSetting(string key, string expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).Get(key);
Assert.That(actual?.Value, Is.EqualTo(expected));
}
[TestCase(null, null)]
[TestCase("", null)]
[TestCase("Setting1", "Set")]
public async Task SettingsHelper_SetSetting(string key, string expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).SetSetting(key, "FakeValue");
Assert.That(actual?.Value, Is.EqualTo(expected));
}
[TestCase("Setting1", true)]
public async Task SettingsHelper_DeleteSetting(string key, bool expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).DeleteSetting(key);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("String1:Key1", "Default", "Value1")]
[TestCase("String1:Key2", "Default", "Value2")]
[TestCase("String2:Key1", "Default", "Value3")]
[TestCase("String2:Key2", "Default", "Default")]
[TestCase("Release:Version", "v0.0.0", "v1.0.0")]
[TestCase("Release:Plugin:Version", "v0.0.0", "v3.0.0")]
public async Task SettingsHelper_GetOrDefault(string key, string defaultValue, string expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("Int1:Key1", 999, 1)]
[TestCase("Int1:Key2", 999, 2)]
[TestCase("Int2:Key1", 999, 3)]
[TestCase("Int2:Key2", 999, 999)]
public async Task SettingsHelper_GetOrDefault(string key, int defaultValue, int expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("Long1:Key1", 999, 4)]
[TestCase("Long1:Key2", 999, 5)]
[TestCase("Long2:Key1", 999, 6)]
[TestCase("Long2:Key2", 999, 999)]
public async Task SettingsHelper_GetOrDefault(string key, long defaultValue, long expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("Decimal1:Key1", 999, 4.12)]
[TestCase("Decimal1:Key2", 999, 5.45)]
[TestCase("Decimal2:Key1", 999, 6.733)]
[TestCase("Decimal2:Key2", 999, 999)]
public async Task SettingsHelper_GetOrDefault(string key, decimal defaultValue, decimal expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("Double1:Key1", 999, 4.12)]
[TestCase("Double1:Key2", 999, 5.45)]
[TestCase("Double2:Key1", 999, 6.733)]
[TestCase("Double2:Key2", 999, 999)]
public async Task SettingsHelper_GetOrDefault(string key, double defaultValue, double expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("Boolean1:Key1", false, true)]
[TestCase("Boolean1:Key2", false, false)]
[TestCase("Boolean2:Key1", false, true)]
[TestCase("Boolean2:Key2", true, true)]
public async Task SettingsHelper_GetOrDefault(string key, bool defaultValue, bool expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(expected));
}
[TestCase("DateTime1:Key1", null, "1987-11-20 08:00:00")]
[TestCase("DateTime1:Key2", null, "2000-08-12 12:00:00")]
[TestCase("DateTime2:Key1", null, "2018-01-01 20:30:10")]
[TestCase("DateTime2:Key2", null, null)]
[TestCase("DateTime3:Key3", "2350-05-05 00:05:12", "2350-05-05 00:05:12")]
public async Task SettingsHelper_GetOrDefault(string key, DateTime? defaultValue, string? expected)
{
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
Assert.That(actual, Is.EqualTo(!string.IsNullOrWhiteSpace(expected) ? DateTime.Parse(expected) : null));
}
[TestCase("1.0.0", 3, "1.0.0")]
[TestCase("1.0.0", 4, "1.0.0.0")]
[TestCase("1.0.0.0", 3, "1.0.0")]
[TestCase("1.0.0.0", 4, "1.0.0.0")]
[TestCase("1.2.3.4", 2, "1.2")]
public async Task SettingsHelper_GetReleaseVersion(string version, int places, string expected)
{
var environment = Substitute.For<IEnvironmentWrapper>();
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
{
{ "Release:Version", version },
});
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
var actual = await new SettingsHelper(_database, config, _logger).GetReleaseVersion(places);
Assert.That(actual, Is.EqualTo(expected));
}
}
}

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.UnitTests.Helpers;
@@ -6,8 +7,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
{
public class UrlHelperTests
{
private readonly IConfigHelper _config = Substitute.For<IConfigHelper>();
private readonly IGalleryHelper _gallery = Substitute.For<IGalleryHelper>();
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
public UrlHelperTests()
{
@@ -16,7 +16,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[SetUp]
public void Setup()
{
_config.GetOrDefault("Settings:Force_Https", Arg.Any<bool>()).Returns(false);
_settings.GetOrDefault(Settings.Basic.ForceHttps, Arg.Any<bool>()).Returns(false);
}
[TestCase("http", "unittest.com", null, "http://unittest.com/")]
@@ -33,13 +33,13 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("https", "mobile.unittest.org", "/unittest?unit=test&blaa=test", "https://mobile.unittest.org/unittest?unit=test&blaa=test")]
public void UrlHelper_GenerateBaseUrl(string scheme, string host, string? querystring, string expected)
{
_config.GetOrDefault("Settings:Base_Url", Arg.Any<string>()).Returns(host);
_settings.GetOrDefault(Settings.Basic.BaseUrl, Arg.Any<string>()).Returns(host);
var mockContext = MockData.MockHttpContext();
mockContext.Request.Scheme = scheme;
mockContext.Request.Host = new HostString(host);
var actual = new UrlHelper(_config).GenerateBaseUrl(mockContext?.Request, querystring);
var actual = new UrlHelper(_settings).GenerateBaseUrl(mockContext?.Request, querystring);
Assert.That(actual, Is.EqualTo(expected));
}
@@ -59,7 +59,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
return new KeyValuePair<string, string>(val.FirstOrDefault() ?? "default", val.LastOrDefault() ?? "default");
})?.ToList();
var actual = new UrlHelper(_config).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
var actual = new UrlHelper(_settings).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
Assert.That(actual, Is.EqualTo(expected));
}
@@ -80,7 +80,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
return new KeyValuePair<string, string>(val.FirstOrDefault() ?? "default", val.LastOrDefault() ?? "default");
})?.ToList();
var actual = new UrlHelper(_config).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
var actual = new UrlHelper(_settings).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
Assert.That(actual, Is.EqualTo(expected));
}
@@ -100,7 +100,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
return new KeyValuePair<string, string>(val.FirstOrDefault() ?? "default", val.LastOrDefault() ?? "default");
})?.ToList();
var actual = new UrlHelper(_config).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
var actual = new UrlHelper(_settings).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
Assert.That(actual, Is.EqualTo(expected));
}
@@ -114,7 +114,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
[TestCase("https://test.com/", "test.com")]
public void UrlHelper_ExtractHost(string host, string expected)
{
var actual = new UrlHelper(_config).ExtractHost(host);
var actual = new UrlHelper(_settings).ExtractHost(host);
Assert.That(actual, Is.EqualTo(expected));
}
@@ -129,7 +129,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
mockContext.Request.Host = new HostString("unit.test.com");
mockContext.Request.QueryString = new QueryString(queryString);
var actual = new UrlHelper(_config).ExtractQueryValue(mockContext?.Request, key);
var actual = new UrlHelper(_settings).ExtractQueryValue(mockContext?.Request, key);
Assert.That(actual, Is.EqualTo(expected));
}
}

View File

@@ -17,7 +17,7 @@
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,52 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
namespace WeddingShare.Attributes
{
public class AllowGuestCreateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
try
{
var request = filterContext.HttpContext.Request;
var galleryId = (request.Query.ContainsKey("id") && !string.IsNullOrWhiteSpace(request.Query["id"])) ? request.Query["id"].ToString().ToLower() : "default";
if (!string.Equals("default", galleryId, StringComparison.OrdinalIgnoreCase))
{
var user = filterContext?.HttpContext?.User;
if (user?.Identity == null || !user.Identity.IsAuthenticated)
{
var configHelper = filterContext.HttpContext.RequestServices.GetService<IConfigHelper>();
if (configHelper != null)
{
if (configHelper.GetOrDefault("Settings:Disable_Guest_Gallery_Creation", true))
{
var databaseHelper = filterContext.HttpContext.RequestServices.GetService<IDatabaseHelper>();
if (databaseHelper != null)
{
var gallery = databaseHelper.GetGallery(galleryId).Result;
if (gallery == null)
{
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.GalleryCreationNotAllowed }, false);
}
}
}
}
}
}
}
catch (Exception ex)
{
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
if (logger != null)
{
logger.LogError(ex, $"Failed to check guest creation - {ex?.Message}");
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WeddingShare.Enums;
using WeddingShare.Extensions;
using WeddingShare.Helpers;
namespace WeddingShare.Attributes
{
public class RequiresRoleAttribute : ActionFilterAttribute
{
public UserLevel User { get; set; } = UserLevel.Basic;
public AccessPermissions Permission { get; set; } = AccessPermissions.Login;
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
try
{
var level = filterContext.HttpContext?.User?.Identity?.GetUserLevel() ?? UserLevel.Basic;
if (level < this.User)
{
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.Unauthorized }, false);
}
var pemissions = filterContext.HttpContext?.User?.Identity?.GetUserPermissions() ?? AccessPermissions.None;
if (!pemissions.HasFlag(this.Permission))
{
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.Unauthorized }, false);
}
}
catch (Exception ex)
{
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
if (logger != null)
{
logger.LogError(ex, $"Failed to validate user role - {ex?.Message}");
}
}
}
}
}

View File

@@ -1,7 +1,9 @@
using System.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
namespace WeddingShare.Attributes
{
@@ -11,42 +13,67 @@ namespace WeddingShare.Attributes
{
try
{
int? galleryId = null;
var request = filterContext.HttpContext.Request;
var databaseHelper = filterContext.HttpContext.RequestServices.GetService<IDatabaseHelper>();
if (databaseHelper != null)
{
var galleryIdentifier = (request.Query.ContainsKey("identifier") && !string.IsNullOrWhiteSpace(request.Query["identifier"])) ? request.Query["identifier"].ToString().ToLower() : null;
if (!string.IsNullOrWhiteSpace(galleryIdentifier))
{
galleryId = databaseHelper.GetGalleryId(galleryIdentifier).Result;
}
var galleryHelper = filterContext.HttpContext.RequestServices.GetService<IGalleryHelper>();
if (galleryHelper != null)
{
var galleryId = (request.Query.ContainsKey("id") && !string.IsNullOrWhiteSpace(request.Query["id"])) ? request.Query["id"].ToString().ToLower() : "default";
var encryptionHelper = filterContext.HttpContext.RequestServices.GetService<IEncryptionHelper>();
if (encryptionHelper != null)
if (galleryId == null)
{
var key = request.Query.ContainsKey("key") ? request.Query["key"].ToString() : string.Empty;
var isEncrypted = request.Query.ContainsKey("enc") ? bool.Parse(request.Query["enc"].ToString().ToLower()) : false;
if (!isEncrypted && !string.IsNullOrWhiteSpace(key) && encryptionHelper.IsEncryptionEnabled())
{
var queryString = HttpUtility.ParseQueryString(request.QueryString.ToString());
queryString.Set("enc", "true");
queryString.Set("key", encryptionHelper.Encrypt(key));
filterContext.Result = new RedirectResult($"/Gallery?{queryString.ToString()}");
}
else
var galleryName = (request.Query.ContainsKey("id") && !string.IsNullOrWhiteSpace(request.Query["id"])) ? request.Query["id"].ToString().ToLower() : "default";
if (!string.IsNullOrWhiteSpace(galleryName))
{
var secretKey = galleryHelper.GetSecretKey(galleryId).Result ?? string.Empty;
if (!string.IsNullOrWhiteSpace(secretKey))
{
secretKey = encryptionHelper.IsEncryptionEnabled() ? encryptionHelper.Encrypt(secretKey) : secretKey;
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
{
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
if (logger != null)
{
logger.LogWarning($"A request was made to an endpoint with an invalid secure key");
}
galleryId = (databaseHelper?.GetGalleryIdByName(galleryName)?.Result) ?? 1;
}
}
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.InvalidSecretKey }, false);
if (galleryId != null)
{
var gallery = databaseHelper?.GetGallery(galleryId.Value).Result;
if (gallery != null)
{
var encryptionHelper = filterContext.HttpContext.RequestServices.GetService<IEncryptionHelper>();
if (encryptionHelper != null)
{
var key = request.Query.ContainsKey("key") ? request.Query["key"].ToString() : string.Empty;
var isEncrypted = request.Query.ContainsKey("enc") ? bool.Parse(request.Query["enc"].ToString().ToLower()) : false;
if (!isEncrypted && !string.IsNullOrWhiteSpace(key) && encryptionHelper.IsEncryptionEnabled())
{
var queryString = HttpUtility.ParseQueryString(request.QueryString.ToString());
queryString.Set("enc", "true");
queryString.Set("key", encryptionHelper.Encrypt(key));
filterContext.Result = new RedirectResult($"/Gallery?{queryString.ToString()}");
}
else
{
var settingsHelper = filterContext.HttpContext.RequestServices.GetService<ISettingsHelper>();
if (settingsHelper != null)
{
var secretKey = settingsHelper.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, galleryId).Result ?? string.Empty;
if (!string.IsNullOrWhiteSpace(secretKey))
{
secretKey = encryptionHelper.IsEncryptionEnabled() ? encryptionHelper.Encrypt(secretKey) : secretKey;
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
{
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
if (logger != null)
{
logger.LogWarning($"A request was made to an endpoint with an invalid secure key");
}
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.InvalidSecretKey }, false);
}
}
}
}
}
}

View File

@@ -1,25 +1,43 @@
using NCrontab;
using WeddingShare.Constants;
using WeddingShare.Helpers;
namespace WeddingShare.BackgroundWorkers
{
public sealed class CleanupService(IWebHostEnvironment hostingEnvironment, IConfigHelper configHelper, IFileHelper fileHelper, ILogger<CleanupService> logger) : BackgroundService
public sealed class CleanupService(IWebHostEnvironment hostingEnvironment, ISettingsHelper settingsHelper, IFileHelper fileHelper, ILogger<CleanupService> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var cron = configHelper.GetOrDefault("BackgroundServices:Schedules:Cleanup", "0 4 * * *");
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
await Task.Delay((int)TimeSpan.FromSeconds(10).TotalMilliseconds, stoppingToken);
while (!stoppingToken.IsCancellationRequested)
var enabled = await settingsHelper.GetOrDefault(BackgroundServices.Cleanup.Enabled, true);
if (enabled)
{
var now = DateTime.Now;
var nextExecutionTime = schedule.GetNextOccurrence(now);
var waitTime = nextExecutionTime - now;
await Task.Delay(waitTime, stoppingToken);
var cron = await settingsHelper.GetOrDefault(BackgroundServices.Cleanup.Schedule, "0 4 * * *");
var nextExecutionTime = DateTime.Now.AddSeconds(10);
await Cleanup();
while (!stoppingToken.IsCancellationRequested)
{
var currentCron = await settingsHelper.GetOrDefault(BackgroundServices.Cleanup.Schedule, "0 4 * * *");
var now = DateTime.Now;
if (now >= nextExecutionTime)
{
await Cleanup();
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
nextExecutionTime = schedule.GetNextOccurrence(now);
}
else
{
if (!currentCron.Equals(cron))
{
nextExecutionTime = DateTime.Now;
}
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
cron = currentCron;
}
}
}
@@ -29,7 +47,7 @@ namespace WeddingShare.BackgroundWorkers
{
var paths = new List<string>()
{
Path.Combine(hostingEnvironment.WebRootPath, "temp")
Path.Combine(hostingEnvironment.WebRootPath, Directories.TempFiles)
};
if (paths != null)

View File

@@ -1,6 +1,7 @@
using NCrontab;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using WeddingShare.Constants;
using WeddingShare.Enums;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
@@ -8,68 +9,189 @@ using WeddingShare.Models.Database;
namespace WeddingShare.BackgroundWorkers
{
public sealed class DirectoryScanner(IWebHostEnvironment hostingEnvironment, IConfigHelper configHelper, IGalleryHelper galleryHelper, IDatabaseHelper databaseHelper, IFileHelper fileHelper, IImageHelper imageHelper, ILogger<DirectoryScanner> logger) : BackgroundService
public sealed class DirectoryScanner(IWebHostEnvironment hostingEnvironment, ISettingsHelper settingsHelper, IDatabaseHelper databaseHelper, IFileHelper fileHelper, IImageHelper imageHelper, ILogger<DirectoryScanner> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var cron = configHelper.GetOrDefault("BackgroundServices:Schedules:Directory_Scanner", "*/30 * * * *");
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
await Task.Delay((int)TimeSpan.FromSeconds(10).TotalMilliseconds, stoppingToken);
await ScanForFiles();
while (!stoppingToken.IsCancellationRequested)
var enabled = await settingsHelper.GetOrDefault(BackgroundServices.DirectoryScanner.Enabled, true);
if (enabled)
{
var now = DateTime.Now;
var nextExecutionTime = schedule.GetNextOccurrence(now);
var waitTime = nextExecutionTime - now;
await Task.Delay(waitTime, stoppingToken);
var cron = await settingsHelper.GetOrDefault(BackgroundServices.DirectoryScanner.Schedule, "*/30 * * * *");
var nextExecutionTime = DateTime.Now.AddSeconds(10);
await ScanForFiles();
while (!stoppingToken.IsCancellationRequested)
{
var currentCron = await settingsHelper.GetOrDefault(BackgroundServices.DirectoryScanner.Schedule, "*/30 * * * *");
var now = DateTime.Now;
if (now >= nextExecutionTime)
{
await ScanForFiles();
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
nextExecutionTime = schedule.GetNextOccurrence(now);
}
else
{
if (!currentCron.Equals(cron))
{
nextExecutionTime = DateTime.Now;
}
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
cron = currentCron;
}
}
}
private async Task ScanForFiles()
{
await Task.Run(async () =>
if (Startup.Ready)
{
if (Startup.Ready)
{
var thumbnailsDirectory = Path.Combine(hostingEnvironment.WebRootPath, "thumbnails");
fileHelper.CreateDirectoryIfNotExists(thumbnailsDirectory);
await this.ScanGalleryImages();
await this.ScanCustomResources();
}
else
{
logger.LogInformation($"Skipping directory scan, application not ready yet");
}
}
var uploadsDirectory = Path.Combine(hostingEnvironment.WebRootPath, "uploads");
if (fileHelper.DirectoryExists(uploadsDirectory))
private async Task ScanGalleryImages()
{
var thumbnailsDirectory = Path.Combine(hostingEnvironment.WebRootPath, Directories.Thumbnails);
fileHelper.CreateDirectoryIfNotExists(thumbnailsDirectory);
var uploadsDirectory = Path.Combine(hostingEnvironment.WebRootPath, Directories.Uploads);
if (fileHelper.DirectoryExists(uploadsDirectory))
{
var galleryDirs = fileHelper.GetDirectories(uploadsDirectory, "*", SearchOption.TopDirectoryOnly)?.Where(x => !Path.GetFileName(x).StartsWith("."));
if (galleryDirs != null)
{
foreach (var galleryDir in galleryDirs)
{
var searchPattern = !configHelper.GetOrDefault("Settings:Single_Gallery_Mode", false) ? "*" : "default";
var galleries = fileHelper.GetDirectories(uploadsDirectory, searchPattern, SearchOption.TopDirectoryOnly)?.Where(x => !Path.GetFileName(x).StartsWith("."));
if (galleries != null)
try
{
foreach (var gallery in galleries)
var identifier = Path.GetFileName(galleryDir).ToLower();
var galleryId = await databaseHelper.GetGalleryId(identifier);
if (galleryId != null)
{
try
var galleryItem = await databaseHelper.GetGallery(galleryId.Value);
if (galleryItem == null)
{
var id = Path.GetFileName(gallery).ToLower();
var galleryItem = await databaseHelper.GetGallery(id);
if (galleryItem == null)
if (await databaseHelper.GetGalleryCount() < await settingsHelper.GetOrDefault(Settings.Basic.MaxGalleryCount, 1000000))
{
galleryItem = await databaseHelper.AddGallery(new GalleryModel()
{
Name = id
Name = identifier,
Owner = 0
});
}
}
if (galleryItem != null)
if (galleryItem != null)
{
var allowedFileTypes = settingsHelper.GetOrDefault(Settings.Gallery.AllowedFileTypes, ".jpg,.jpeg,.png,.mp4,.mov", galleryItem?.Id).Result.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var galleryItems = await databaseHelper.GetAllGalleryItems(galleryItem.Id);
if (Path.Exists(galleryDir))
{
var allowedFileTypes = galleryHelper.GetConfig(galleryItem.Name, "Gallery:Allowed_File_Types", ".jpg,.jpeg,.png,.mp4,.mov").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var galleryItems = await databaseHelper.GetAllGalleryItems(galleryItem.Id);
if (Path.Exists(gallery))
var approvedFiles = fileHelper.GetFiles(galleryDir, "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
if (approvedFiles != null)
{
var approvedFiles = fileHelper.GetFiles(gallery, "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
if (approvedFiles != null)
foreach (var file in approvedFiles)
{
foreach (var file in approvedFiles)
try
{
var filename = Path.GetFileName(file);
var g = galleryItems.FirstOrDefault(x => string.Equals(x.Title, filename, StringComparison.OrdinalIgnoreCase));
if (g == null)
{
g = await databaseHelper.AddGalleryItem(new GalleryItemModel()
{
GalleryId = galleryItem.Id,
Title = filename,
Checksum = await fileHelper.GetChecksum(file),
MediaType = imageHelper.GetMediaType(file),
State = GalleryItemState.Approved,
UploadedDate = await fileHelper.GetCreationDatetime(file),
FileSize = fileHelper.FileSize(file)
});
}
var thumbnailDir = Path.Combine(thumbnailsDirectory, galleryItem.Name);
var thumbnailPath = Path.Combine(thumbnailDir, $"{Path.GetFileNameWithoutExtension(file)}.webp");
if (!fileHelper.FileExists(thumbnailPath))
{
fileHelper.CreateDirectoryIfNotExists(thumbnailDir);
await imageHelper.GenerateThumbnail(file, thumbnailPath, settingsHelper.GetOrDefault(Settings.Basic.ThumbnailSize, 720).Result);
fileHelper.DeleteFileIfExists(Path.Combine(thumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(file)}.webp"));
}
else
{
using (var img = await Image.LoadAsync(thumbnailPath))
{
var width = img.Width;
img.Mutate(x => x.AutoOrient());
if (width != img.Width)
{
await img.SaveAsWebpAsync(thumbnailPath);
}
}
}
if (g != null)
{
var updated = false;
if (g.UploadedDate == null)
{
g.UploadedDate = new FileInfo(file).CreationTimeUtc;
updated = true;
}
if (g.MediaType == MediaType.Unknown)
{
g.MediaType = imageHelper.GetMediaType(file);
updated = true;
}
if (g.Orientation == ImageOrientation.None)
{
g.Orientation = await imageHelper.GetOrientation(thumbnailPath);
updated = true;
}
if (g.FileSize == 0)
{
g.FileSize = fileHelper.FileSize(file);
updated = true;
}
if (updated)
{
await databaseHelper.EditGalleryItem(g);
}
}
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred while scanning file '{file}'");
}
}
}
if (Path.Exists(Path.Combine(galleryDir, "Pending")))
{
var pendingFiles = fileHelper.GetFiles(Path.Combine(galleryDir, "Pending"), "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
if (pendingFiles != null)
{
foreach (var file in pendingFiles)
{
try
{
@@ -82,29 +204,11 @@ namespace WeddingShare.BackgroundWorkers
Title = filename,
Checksum = await fileHelper.GetChecksum(file),
MediaType = imageHelper.GetMediaType(file),
State = GalleryItemState.Approved
State = GalleryItemState.Pending,
UploadedDate = await fileHelper.GetCreationDatetime(file),
FileSize = new FileInfo(file).Length
});
}
var thumbnailPath = Path.Combine(thumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(file)}.webp");
if (!fileHelper.FileExists(thumbnailPath))
{
await imageHelper.GenerateThumbnail(file, thumbnailPath, configHelper.GetOrDefault("Settings:Thumbnail_Size", 720));
}
else
{
using (var img = await Image.LoadAsync(thumbnailPath))
{
var width = img.Width;
img.Mutate(x => x.AutoOrient());
if (width != img.Width)
{
await img.SaveAsWebpAsync(thumbnailPath);
}
}
}
}
catch (Exception ex)
{
@@ -112,52 +216,42 @@ namespace WeddingShare.BackgroundWorkers
}
}
}
if (Path.Exists(Path.Combine(gallery, "Pending")))
{
var pendingFiles = fileHelper.GetFiles(Path.Combine(gallery, "Pending"), "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
if (pendingFiles != null)
{
foreach (var file in pendingFiles)
{
try
{
var filename = Path.GetFileName(file);
if (!galleryItems.Exists(x => string.Equals(x.Title, filename, StringComparison.OrdinalIgnoreCase)))
{
await databaseHelper.AddGalleryItem(new GalleryItemModel()
{
GalleryId = galleryItem.Id,
Title = filename,
Checksum = await fileHelper.GetChecksum(file),
MediaType = imageHelper.GetMediaType(file),
State = GalleryItemState.Pending
});
}
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred while scanning file '{file}'");
}
}
}
}
}
}
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred while scanning directory '{gallery}'");
}
}
}
catch (Exception ex)
{
logger.LogError(ex, $"An error occurred while scanning directory '{galleryDir}'");
}
}
}
else
{
logger.LogInformation($"Skipping directory scan, application not ready yet");
}
}
private async Task ScanCustomResources()
{
var existing = await databaseHelper.GetAllCustomResources();
var customResourcesDirectory = Path.Combine(hostingEnvironment.WebRootPath, Directories.CustomResources);
foreach (var resource in fileHelper.GetFiles(customResourcesDirectory))
{
try
{
var filename = Path.GetFileName(resource);
if (!existing.Any(x => filename.Equals(x.FileName, StringComparison.OrdinalIgnoreCase)))
{
await databaseHelper.AddCustomResource(new CustomResourceModel()
{
FileName = filename,
UploadedBy = "DirectoryScanner",
Owner = 0
});
}
}
});
catch { }
}
}
}
}

View File

@@ -1,28 +1,48 @@
using System.Text;
using NCrontab;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.Helpers.Notifications;
namespace WeddingShare.BackgroundWorkers
{
public sealed class NotificationReport(IConfigHelper configHelper, IDatabaseHelper databaseHelper, ISmtpClientWrapper smtpHelper, ILoggerFactory loggerFactory) : BackgroundService
public sealed class NotificationReport(ISettingsHelper settingsHelper, IDatabaseHelper databaseHelper, ISmtpClientWrapper smtpHelper, ILoggerFactory loggerFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (configHelper.GetOrDefault("Settings:Email_Report", true) && configHelper.GetOrDefault("Notifications:Smtp:Enabled", false))
{
var cron = configHelper.GetOrDefault("BackgroundServices:Schedules:Email_Report", "0 0 * * *");
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
var enabled = await settingsHelper.GetOrDefault(BackgroundServices.EmailReport.Enabled, true);
if (enabled)
{
var cron = await settingsHelper.GetOrDefault(BackgroundServices.EmailReport.Schedule, "0 0 * * *");
var nextExecutionTime = DateTime.Now.AddSeconds(10);
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
var nextExecutionTime = schedule.GetNextOccurrence(now);
var waitTime = nextExecutionTime - now;
await Task.Delay(waitTime, stoppingToken);
var currentCron = await settingsHelper.GetOrDefault(BackgroundServices.EmailReport.Schedule, "0 0 * * *");
await SendReport();
var now = DateTime.Now;
if (now >= nextExecutionTime)
{
if (await settingsHelper.GetOrDefault(Settings.Basic.EmailReport, true) && await settingsHelper.GetOrDefault(Notifications.Smtp.Enabled, false))
{
await SendReport();
}
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
nextExecutionTime = schedule.GetNextOccurrence(now);
}
else
{
if (!currentCron.Equals(cron))
{
nextExecutionTime = DateTime.Now;
}
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
cron = currentCron;
}
}
}
@@ -37,19 +57,23 @@ namespace WeddingShare.BackgroundWorkers
var builder = new StringBuilder();
builder.AppendLine($"<h1>You have items pending review!</h1>");
foreach (var item in pendingItems.GroupBy(x => x.GalleryName).OrderBy(x => x.Key))
foreach (var item in pendingItems.GroupBy(x => x.GalleryId).OrderByDescending(x => x.Count()))
{
try
{
builder.AppendLine($"<p style=\"font-size: 16pt;\">{item.Key} - Pending Items ({item.Count()})</p>");
}
catch (Exception ex)
{
loggerFactory.CreateLogger<NotificationReport>().LogError(ex, $"Failed to build gallery report for id '{item?.Key}' - {ex?.Message}");
var gallery = await databaseHelper.GetGallery(item.Key);
if (gallery != null)
{
try
{
builder.AppendLine($"<p style=\"font-size: 16pt;\">{gallery.Name} - Pending Items ({item.Count()})</p>");
}
catch (Exception ex)
{
loggerFactory.CreateLogger<NotificationReport>().LogError(ex, $"Failed to build gallery report for '{gallery.Name}' - {ex?.Message}");
}
}
}
var sent = await new EmailHelper(configHelper, smtpHelper, loggerFactory.CreateLogger<EmailHelper>()).Send("Pending Items Report", builder.ToString());
var sent = await new EmailHelper(settingsHelper, smtpHelper, loggerFactory.CreateLogger<EmailHelper>()).Send("Pending Items Report", builder.ToString());
if (!sent)
{
loggerFactory.CreateLogger<NotificationReport>().LogWarning($"Failed to send notification report");

View File

@@ -1,4 +1,5 @@
using WeddingShare.Helpers;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.Helpers.Dbup;
@@ -6,21 +7,26 @@ namespace WeddingShare.Configurations
{
internal static class DatabaseConfiguration
{
public static void AddDatabaseConfiguration(this IServiceCollection services, ConfigHelper config)
public static IDatabaseHelper AddDatabaseConfiguration(this IServiceCollection services, IConfigHelper config, ILoggerFactory loggerFactory)
{
switch (config.GetOrDefault("Settings:Database:Type", "sqlite")?.ToLower())
IDatabaseHelper helper;
var databaseType = config.GetOrDefault(Settings.Database.Type, "sqlite");
switch (databaseType?.ToLower())
{
case "sqlite":
services.AddSingleton<IDatabaseHelper, SQLiteDatabaseHelper>();
break;
case "mysql":
services.AddSingleton<IDatabaseHelper, MySqlDatabaseHelper>();
helper = new MySqlDatabaseHelper(config, loggerFactory.CreateLogger<MySqlDatabaseHelper>());
break;
default:
services.AddSingleton<IDatabaseHelper, SQLiteDatabaseHelper>();
helper = new SQLiteDatabaseHelper(config, loggerFactory.CreateLogger<SQLiteDatabaseHelper>());
break;
}
services.AddHostedService<DbupMigrator>();
return helper;
}
}
}

View File

@@ -8,13 +8,15 @@ namespace WeddingShare.Configurations
{
services.AddSingleton<IConfigHelper, ConfigHelper>();
services.AddSingleton<IEnvironmentWrapper, EnvironmentWrapper>();
services.AddSingleton<IGalleryHelper, GalleryHelper>();
services.AddSingleton<ISettingsHelper, SettingsHelper>();
services.AddSingleton<IImageHelper, ImageHelper>();
services.AddSingleton<IFileHelper, FileHelper>();
services.AddSingleton<IDeviceDetector, DeviceDetector>();
services.AddSingleton<ISmtpClientWrapper, SmtpClientWrapper>();
services.AddSingleton<IEncryptionHelper, EncryptionHelper>();
services.AddSingleton<IUrlHelper, UrlHelper>();
services.AddSingleton<IAuditHelper, AuditHelper>();
services.AddSingleton<ILanguageHelper, LanguageHelper>();
}
}
}

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Localization;
using System.Globalization;
using System.Globalization;
using Microsoft.AspNetCore.Localization;
using WeddingShare.Constants;
using WeddingShare.Helpers;
namespace WeddingShare.Configurations
@@ -8,7 +9,7 @@ namespace WeddingShare.Configurations
{
public static string CurrentCulture = "en-GB";
public static void AddLocalizationConfiguration(this IServiceCollection services, ConfigHelper config)
public static void AddLocalizationConfiguration(this IServiceCollection services, SettingsHelper settings)
{
services.AddLocalization(options =>
{
@@ -16,30 +17,18 @@ namespace WeddingShare.Configurations
});
services.Configure<RequestLocalizationOptions>(options => {
var language = config.GetOrDefault("Settings:Language", string.Empty);
var supportedCultures = new[]
{
new CultureInfo("en-GB"),
new CultureInfo("fr-FR"),
new CultureInfo("de-DE"),
new CultureInfo("nl-NL"),
new CultureInfo("es-ES"),
new CultureInfo("sv-SE")
};
var supportedCultures = new LanguageHelper().DetectSupportedCultures();
var language = settings.GetOrDefault(Settings.Languages.Default, "en-GB").Result;
CurrentCulture = GetDefaultCulture(supportedCultures, language);
if (!string.IsNullOrWhiteSpace(language) && !string.Equals("en-GB", CurrentCulture, StringComparison.OrdinalIgnoreCase))
{
supportedCultures = supportedCultures?.Where(x => CultureMatches(x, CurrentCulture))?.ToArray();
}
options.DefaultRequestCulture = new RequestCulture(CurrentCulture);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
}
private static string GetDefaultCulture(CultureInfo[] supported, string key)
private static string GetDefaultCulture(List<CultureInfo> supported, string key)
{
try
{

View File

@@ -8,21 +8,21 @@ namespace WeddingShare.Configurations
{
private const int CLIENT_DEFAULT_TIMEOUT = 10;
public static void AddNotificationConfiguration(this IServiceCollection services, ConfigHelper config)
public static void AddNotificationConfiguration(this IServiceCollection services, SettingsHelper settings)
{
services.AddSingleton<INotificationHelper, NotificationBroker>();
services.AddNtfyConfiguration(config);
services.AddGotifyConfiguration(config);
services.AddNtfyConfiguration(settings);
services.AddGotifyConfiguration(settings);
}
public static void AddNtfyConfiguration(this IServiceCollection services, ConfigHelper config)
public static void AddNtfyConfiguration(this IServiceCollection services, SettingsHelper settings)
{
services.AddHttpClient("NtfyClient", (serviceProvider, httpClient) =>
{
var endpoint = config.GetOrDefault("Notifications:Ntfy:Endpoint", string.Empty);
var endpoint = settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, string.Empty).Result;
if (!string.IsNullOrWhiteSpace(endpoint))
{
var token = config.GetOrDefault("Notifications:Ntfy:Token", string.Empty);
var token = settings.GetOrDefault(Constants.Notifications.Ntfy.Token, string.Empty).Result;
if (!string.IsNullOrWhiteSpace(token))
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
@@ -34,11 +34,11 @@ namespace WeddingShare.Configurations
});
}
public static void AddGotifyConfiguration(this IServiceCollection services, ConfigHelper config)
public static void AddGotifyConfiguration(this IServiceCollection services, SettingsHelper settings)
{
services.AddHttpClient("GotifyClient", (serviceProvider, httpClient) =>
{
var endpoint = config.GetOrDefault("Notifications:Gotify:Endpoint", string.Empty);
var endpoint = settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, string.Empty).Result;
if (!string.IsNullOrWhiteSpace(endpoint))
{
httpClient.BaseAddress = new Uri(endpoint);

View File

@@ -0,0 +1,17 @@
using WeddingShare.Constants;
using WeddingShare.Helpers;
namespace WeddingShare.Configurations
{
internal static class WebClientConfiguration
{
public static void AddWebClientConfiguration(this IServiceCollection services, IConfigHelper config)
{
services.AddHttpClient("SponsorsClient", (client) =>
{
client.BaseAddress = new Uri(config.GetOrDefault(Sponsors.Url, "http://localhost:5000/"));
client.Timeout = TimeSpan.FromSeconds(5);
});
}
}
}

View File

@@ -0,0 +1,26 @@
namespace WeddingShare.Constants
{
public class BackgroundServices
{
public class DirectoryScanner
{
public const string BaseKey = "BackgroundServices:Directory_Scanner:";
public const string Enabled = "BackgroundServices:Directory_Scanner:Enabled";
public const string Schedule = "BackgroundServices:Directory_Scanner:Schedule";
}
public class EmailReport
{
public const string BaseKey = "BackgroundServices:Email_Report:";
public const string Enabled = "BackgroundServices:Email_Report:Enabled";
public const string Schedule = "BackgroundServices:Email_Report:Schedule";
}
public class Cleanup
{
public const string BaseKey = "BackgroundServices:Cleanup:";
public const string Enabled = "BackgroundServices:Cleanup:Enabled";
public const string Schedule = "BackgroundServices:Cleanup:Schedule";
}
}
}

View File

@@ -0,0 +1,12 @@
namespace WeddingShare.Constants
{
public class Directories
{
public const string Config = "config";
public const string CustomResources = "custom_resources";
public const string Images = "images";
public const string TempFiles = "temp";
public const string Thumbnails = "thumbnails";
public const string Uploads = "uploads";
}
}

View File

@@ -0,0 +1,7 @@
namespace WeddingShare.Constants
{
public class FFMPEG
{
public const string InstallPath = "FFMPEG:InstallPath";
}
}

View File

@@ -0,0 +1,43 @@
namespace WeddingShare.Constants
{
public class Notifications
{
public class Alerts
{
public const string FailedLogin = "Notifications:Alerts:Failed_Login";
public const string AccountLockout = "Notifications:Alerts:Account_Lockout";
public const string DestructiveAction = "Notifications:Alerts:Destructive_Action";
public const string PendingReview = "Notifications:Alerts:Pending_Review";
}
public class Gotify
{
public const string Enabled = "Notifications:Gotify:Enabled";
public const string Endpoint = "Notifications:Gotify:Endpoint";
public const string Token = "Notifications:Gotify:Token";
public const string Priority = "Notifications:Gotify:Priority";
}
public class Ntfy
{
public const string Enabled = "Notifications:Ntfy:Enabled";
public const string Endpoint = "Notifications:Ntfy:Endpoint";
public const string Token = "Notifications:Ntfy:Token";
public const string Topic = "Notifications:Ntfy:Topic";
public const string Priority = "Notifications:Ntfy:Priority";
}
public class Smtp
{
public const string Enabled = "Notifications:Smtp:Enabled";
public const string Recipient = "Notifications:Smtp:Recipient";
public const string Host = "Notifications:Smtp:Host";
public const string Port = "Notifications:Smtp:Port";
public const string Username = "Notifications:Smtp:Username";
public const string Password = "Notifications:Smtp:Password";
public const string From = "Notifications:Smtp:From";
public const string DisplayName = "Notifications:Smtp:DisplayName";
public const string UseSSL = "Notifications:Smtp:Use_SSL";
}
}
}

View File

@@ -0,0 +1,7 @@
namespace WeddingShare.Constants
{
public class ProtectedValues
{
public static readonly string[] GalleryNames = [ "All", "Default" ];
}
}

View File

@@ -0,0 +1,7 @@
namespace WeddingShare.Constants
{
public class Release
{
public const string Version = "Release:Version";
}
}

View File

@@ -0,0 +1,26 @@
namespace WeddingShare.Constants
{
public class Security
{
public class Encryption
{
public const string Key = "Security:Encryption:Key";
public const string Salt = "Security:Encryption:Salt";
public const string Iterations = "Security:Encryption:Iterations";
public const string HashType = "Security:Encryption:HashType";
}
public class Headers
{
public const string Enabled = "Security:Headers:Enabled";
public const string XFrameOptions = "Security:Headers:X_Frame_Options";
public const string XContentTypeOptions = "Security:Headers:X_Content_Type_Options";
public const string CSP = "Security:Headers:CSP";
}
public class MultiFactor
{
public const string ResetToDefault = "Security:2FA:Reset_To_Default";
}
}
}

View File

@@ -0,0 +1,134 @@
namespace WeddingShare.Constants
{
public class Settings
{
public const string IsDemoMode = "Settings:Demo_Mode";
public class Account
{
public const string BaseKey = "Settings:Account:";
public const string ShowProfileIcon = "Settings:Account:Show_Profile_Icon";
public const string LockoutAttempts = "Settings:Account:Lockout_Attempts";
public const string LockoutMins = "Settings:Account:Lockout_Mins";
public class Admin
{
public const string Username = "Settings:Account:Admin:Username";
public const string Password = "Settings:Account:Admin:Password";
}
public class Owner
{
public const string BaseKey = "Settings:Account:Owner:";
public const string Username = "Settings:Account:Owner:Username";
public const string Password = "Settings:Account:Owner:Password";
public const string LogPassword = "Settings:Account:Owner:Log_Password";
}
}
public class Basic
{
public const string BaseKey = "Settings:";
public const string Title = "Settings:Title";
public const string Logo = "Settings:Logo";
public const string BaseUrl = "Settings:Base_Url";
public const string ForceHttps = "Settings:Force_Https";
public const string SingleGalleryMode = "Settings:Single_Gallery_Mode";
public const string MaxGalleryCount = "Settings:Max_Gallery_Count";
public const string HomeLink = "Settings:Home_Link";
public const string GuestGalleryCreation = "Settings:Guest_Gallery_Creation";
public const string HideKeyFromQRCode = "Settings:Hide_Key_From_QR_Code";
public const string LinksOpenNewTab = "Settings:Links_Open_New_Tab";
public const string ThumbnailSize = "Settings:Thumbnail_Size";
public const string EmailReport = "Settings:Email_Report";
}
public class Database
{
public const string BaseKey = "Settings:Database:";
public const string Type = "Settings:Database:Type";
public const string ConnectionString = "Settings:Database:Connection_String";
public const string DatabaseName = "Settings:Database:Database_Name";
public const string SyncFromConfig = "Settings:Database:Sync_From_Config";
}
public class Gallery
{
public const string BaseKey = "Settings:Gallery:";
public const string BannerImage = "Settings:Gallery:Banner_Image";
public const string Quote = "Settings:Gallery:Quote";
public const string SecretKey = "Settings:Gallery:Secret_Key";
public const string Columns = "Settings:Gallery:Columns";
public const string ItemsPerPage = "Settings:Gallery:Items_Per_Page";
public const string FullWidth = "Settings:Gallery:Full_Width";
public const string RetainRejectedItems = "Settings:Gallery:Retain_Rejected_Items";
public const string Upload = "Settings:Gallery:Upload";
public const string Download = "Settings:Gallery:Download";
public const string RequireReview = "Settings:Gallery:Require_Review";
public const string ReviewCounter = "Settings:Gallery:Review_Counter";
public const string PreventDuplicates = "Settings:Gallery:Prevent_Duplicates";
public const string IdleRefreshMins = "Settings:Gallery:Idle_Refresh_Mins";
public const string MaxSizeMB = "Settings:Gallery:Max_Size_MB";
public const string MaxFileSizeMB = "Settings:Gallery:Max_File_Size_MB";
public const string DefaultView = "Settings:Gallery:Default_View";
public const string UploadPeriod = "Settings:Gallery:Upload_Period";
public const string AllowedFileTypes = "Settings:Gallery:Allowed_File_Types";
public const string CameraUploads = "Settings:Gallery:Camera_Uploads";
public const string ShowFilters = "Settings:Gallery:Show_Filters";
public class QRCode
{
public const string BaseKey = "Settings:Gallery:QR_Code:";
public const string Enabled = "Settings:Gallery:QR_Code:Enabled";
public const string DefaultView = "Settings:Gallery:QR_Code:Default_View";
public const string DefaultSort = "Settings:Gallery:QR_Code:Default_Sort";
public const string IncludeCulture = "Settings:Gallery:QR_Code:Include_Culture";
}
}
public class GallerySelector
{
public const string BaseKey = "Settings:Gallery_Selector:";
public const string Dropdown = "Settings:Gallery_Selector:Dropdown";
public const string HideDefaultOption = "Settings:Gallery_Selector:Hide_Default_Option";
}
public class IdentityCheck
{
public const string BaseKey = "Settings:Identity_Check:";
public const string Enabled = "Settings:Identity_Check:Enabled";
public const string ShowOnPageLoad = "Settings:Identity_Check:Show_On_Page_Load";
public const string RequireIdentityForUpload = "Settings:Identity_Check:Require_Identity_For_Upload";
}
public class Languages
{
public const string BaseKey = "Settings:Languages:";
public const string Enabled = "Settings:Languages:Enabled";
public const string Default = "Settings:Languages:Default";
}
public class Slideshow
{
public const string BaseKey = "Settings:Slideshow:";
public const string Interval = "Settings:Slideshow:Interval";
public const string Fade = "Settings:Slideshow:Fade";
public const string Limit = "Settings:Slideshow:Limit";
public const string IncludeShareSlide = "Settings:Slideshow:Include_Share_Slide";
}
public class Themes
{
public const string BaseKey = "Settings:Themes:";
public const string Enabled = "Settings:Themes:Enabled";
public const string Default = "Settings:Themes:Default";
}
public class Policies
{
public const string BaseKey = "Settings:Policies:";
public const string Enabled = "Settings:Policies:Enabled";
public const string CookiePolicy = "Settings:Policies:CookiePolicy";
}
}
}

View File

@@ -0,0 +1,13 @@
namespace WeddingShare.Constants
{
public class Sponsors
{
public const string Url = "Sponsors:Url";
public const string Endpoint = "Sponsors:Endpoint";
public class Github
{
public const string ProfileUrl = "Sponsors:Github:ProfileUrl";
}
}
}

View File

@@ -0,0 +1,49 @@
namespace WeddingShare.Constants
{
public class ViewOptions
{
public static IDictionary<string, string> YesNo = new Dictionary<string, string>()
{
{ "Yes", "true" },
{ "No", "false" }
};
public static IDictionary<string, string> YesNoInverted = new Dictionary<string, string>()
{
{ "Yes", "false" },
{ "No", "true" }
};
public static IDictionary<string, string> SingleGalleryMode = new Dictionary<string, string>()
{
{ "Single", "true" },
{ "Multiple", "false" }
};
public static IDictionary<string, string> GallerySelectorDropdown = new Dictionary<string, string>()
{
{ "Dropdown", "true" },
{ "Input", "false" }
};
public static IDictionary<string, string> GalleryWidth = new Dictionary<string, string>()
{
{ "Full Width", "true" },
{ "Default", "false" }
};
public static IDictionary<string, string> GalleryDefaultView = new Dictionary<string, string>()
{
{ "Default", "default" },
{ "Presentation", "presentation" },
{ "Slideshow", "slideshow" }
};
public static IDictionary<string, string> GalleryDefaultSort = new Dictionary<string, string>()
{
{ "Ascending", "0" },
{ "Descending", "1" },
{ "Random", "2" }
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,919 +0,0 @@
using System.IO.Compression;
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using TwoFactorAuthNet;
using WeddingShare.Enums;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.Helpers.Notifications;
using WeddingShare.Models;
using WeddingShare.Models.Database;
using WeddingShare.Views.Admin;
namespace WeddingShare.Controllers
{
[Authorize]
public class AdminController : Controller
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IConfigHelper _config;
private readonly IDatabaseHelper _database;
private readonly IDeviceDetector _deviceDetector;
private readonly IFileHelper _fileHelper;
private readonly INotificationHelper _notificationHelper;
private readonly Helpers.IUrlHelper _url;
private readonly ILogger _logger;
private readonly IStringLocalizer<Lang.Translations> _localizer;
private readonly string TempDirectory;
private readonly string UploadsDirectory;
private readonly string ThumbnailsDirectory;
public AdminController(IWebHostEnvironment hostingEnvironment, IConfigHelper config, IDatabaseHelper database, IDeviceDetector deviceDetector, IFileHelper fileHelper, INotificationHelper notificationHelper, Helpers.IUrlHelper url, ILogger<AdminController> logger, IStringLocalizer<Lang.Translations> localizer)
{
_hostingEnvironment = hostingEnvironment;
_config = config;
_database = database;
_deviceDetector = deviceDetector;
_fileHelper = fileHelper;
_notificationHelper = notificationHelper;
_url = url;
_logger = logger;
_localizer = localizer;
TempDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "temp");
UploadsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
ThumbnailsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "thumbnails");
}
[AllowAnonymous]
[HttpGet]
public IActionResult Login()
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Admin");
}
return View();
}
[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost]
public async Task<IActionResult> Login(LoginModel model)
{
try
{
var user = await _database.GetUser(model.Username);
if (user != null && !user.IsLockedOut)
{
if (await _database.ValidateCredentials(user.Username, model.Password))
{
if (user.FailedLogins > 0)
{
await _database.ResetLockoutCount(user.Id);
}
var mfaSet = !string.IsNullOrEmpty(user.MultiFactorToken);
HttpContext.Session.SetString(SessionKey.MultiFactorTokenSet, mfaSet.ToString().ToLower());
if (mfaSet)
{
return Json(new { success = true, mfa = true });
}
else
{
return Json(new { success = await this.SetUserClaims(this.HttpContext, user), mfa = false });
}
}
else
{
await this.FailedLoginDetected(model, user);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Login_Failed"].Value} - {ex?.Message}");
}
return Json(new { success = false });
}
[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost]
public async Task<IActionResult> ValidateMultifactorAuth(LoginModel model)
{
if (!string.IsNullOrWhiteSpace(model?.Code))
{
try
{
var user = await _database.GetUser(model.Username);
if (user != null && !user.IsLockedOut)
{
if (await _database.ValidateCredentials(user.Username, model.Password))
{
if (user.FailedLogins > 0)
{
await _database.ResetLockoutCount(user.Id);
}
var mfaSet = !string.IsNullOrWhiteSpace(user.MultiFactorToken);
HttpContext.Session.SetString(SessionKey.MultiFactorTokenSet, (!string.IsNullOrEmpty(user.MultiFactorToken)).ToString().ToLower());
if (mfaSet)
{
var tfa = new TwoFactorAuth(_config.GetOrDefault("Settings:Title", "WeddingShare"));
if (tfa.VerifyCode(user.MultiFactorToken, model.Code))
{
return Json(new { success = await this.SetUserClaims(this.HttpContext, user) });
}
}
else
{
return Json(new { success = await this.SetUserClaims(this.HttpContext, user) });
}
}
else
{
await this.FailedLoginDetected(model, user);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Login_Failed"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[Authorize]
[HttpGet]
public async Task<IActionResult> Logout()
{
await this.HttpContext.SignOutAsync();
return RedirectToAction("Index", "Home");
}
[HttpGet]
public async Task<IActionResult> Index()
{
if (User?.Identity == null || !User.Identity.IsAuthenticated)
{
return Redirect("/");
}
var model = new IndexModel();
var deviceType = HttpContext.Session.GetString(SessionKey.DeviceType);
if (string.IsNullOrWhiteSpace(deviceType))
{
deviceType = (await _deviceDetector.ParseDeviceType(Request.Headers["User-Agent"].ToString())).ToString();
HttpContext.Session.SetString(SessionKey.DeviceType, deviceType ?? "Desktop");
}
try
{
var user = await _database.GetUser(int.Parse(((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1"));
if (user != null)
{
if (!_config.GetOrDefault("Settings:Single_Gallery_Mode", false))
{
model.Galleries = await _database.GetAllGalleries();
if (model.Galleries != null)
{
var all = await _database.GetGallery(0);
if (all != null)
{
model.Galleries.Add(all);
}
}
model.PendingRequests = await _database.GetPendingGalleryItems();
}
else
{
var gallery = await _database.GetGallery("default");
if (gallery != null)
{
model.Galleries = new List<GalleryModel>() { gallery };
model.PendingRequests = await _database.GetPendingGalleryItems(gallery.Id);
}
}
model.Users = await _database.GetAllUsers();
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Pending_Uploads_Failed"].Value} - {ex?.Message}");
}
return View(model);
}
[HttpGet]
public async Task<IActionResult> AvailableGalleries()
{
if (User?.Identity == null || !User.Identity.IsAuthenticated)
{
return Redirect("/");
}
List<GalleryModel>? result = null;
try
{
var user = await _database.GetUser(int.Parse(((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1"));
if (user != null)
{
if (!_config.GetOrDefault("Settings:Single_Gallery_Mode", false))
{
result = await _database.GetAllGalleries();
if (result != null)
{
var all = await _database.GetGallery(0);
if (all != null)
{
result.Add(all);
}
}
}
else
{
var gallery = await _database.GetGallery("default");
if (gallery != null)
{
result = new List<GalleryModel>() { gallery };
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Gallery_List_Failed"].Value} - {ex?.Message}");
}
return PartialView(result ?? new List<GalleryModel>());
}
[HttpGet]
public async Task<IActionResult> PendingReviews()
{
if (User?.Identity == null || !User.Identity.IsAuthenticated)
{
return Redirect("/");
}
List<GalleryItemModel>? result = null;
try
{
var user = await _database.GetUser(int.Parse(((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1"));
if (user != null)
{
if (!_config.GetOrDefault("Settings:Single_Gallery_Mode", false))
{
result = await _database.GetPendingGalleryItems();
}
else
{
var gallery = await _database.GetGallery("default");
if (gallery != null)
{
result = await _database.GetPendingGalleryItems(gallery.Id);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Pending_Uploads_Failed"].Value} - {ex?.Message}");
}
return PartialView(result ?? new List<GalleryItemModel>());
}
[HttpGet]
public async Task<IActionResult> UsersList()
{
if (User?.Identity == null || !User.Identity.IsAuthenticated)
{
return Redirect("/");
}
List<UserModel>? result = null;
try
{
var user = await _database.GetUser(int.Parse(((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1"));
if (user != null)
{
result = await _database.GetAllUsers();
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Users_List_Failed"].Value} - {ex?.Message}");
}
return PartialView(result ?? new List<UserModel>());
}
[HttpPost]
public async Task<IActionResult> ReviewPhoto(int id, ReviewAction action)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var review = await _database.GetPendingGalleryItem(id);
if (review != null)
{
var galleryDir = Path.Combine(UploadsDirectory, review.GalleryName);
var reviewFile = Path.Combine(galleryDir, "Pending", review.Title);
if (action == ReviewAction.APPROVED)
{
_fileHelper.MoveFileIfExists(reviewFile, Path.Combine(galleryDir, review.Title));
review.State = GalleryItemState.Approved;
await _database.EditGalleryItem(review);
}
else if (action == ReviewAction.REJECTED)
{
var retain = _config.GetOrDefault("Settings:Gallery:Retain_Rejected_Items", false);
if (retain)
{
var rejectedDir = Path.Combine(galleryDir, "Rejected");
_fileHelper.CreateDirectoryIfNotExists(rejectedDir);
_fileHelper.MoveFileIfExists(reviewFile, Path.Combine(rejectedDir, review.Title));
}
else
{
_fileHelper.DeleteFileIfExists(reviewFile);
}
await _database.DeleteGalleryItem(review);
}
else if (action == ReviewAction.UNKNOWN)
{
throw new Exception(_localizer["Unknown_Review_Action"].Value);
}
return Json(new { success = true, action });
}
else
{
return Json(new { success = false, message = _localizer["Failed_Finding_File"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Reviewing_Media"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpPost]
public async Task<IActionResult> BulkReview(ReviewAction action)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var items = await _database.GetPendingGalleryItems();
if (items != null && items.Any())
{
foreach (var review in items)
{
var galleryDir = Path.Combine(UploadsDirectory, review.GalleryName);
var reviewFile = Path.Combine(galleryDir, "Pending", review.Title);
if (action == ReviewAction.APPROVED)
{
_fileHelper.MoveFileIfExists(reviewFile, Path.Combine(galleryDir, review.Title));
review.State = GalleryItemState.Approved;
await _database.EditGalleryItem(review);
}
else if (action == ReviewAction.REJECTED)
{
var retain = _config.GetOrDefault("Settings:Gallery:Retain_Rejected_Items", false);
if (retain)
{
var rejectedDir = Path.Combine(galleryDir, "Rejected");
_fileHelper.CreateDirectoryIfNotExists(rejectedDir);
_fileHelper.MoveFileIfExists(reviewFile, Path.Combine(rejectedDir, review.Title));
}
else
{
_fileHelper.DeleteFileIfExists(reviewFile);
}
await _database.DeleteGalleryItem(review);
}
else if (action == ReviewAction.UNKNOWN)
{
throw new Exception(_localizer["Unknown_Review_Action"].Value);
}
}
}
return Json(new { success = true, action });
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Reviewing_Media"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpPost]
public async Task<IActionResult> AddGallery(GalleryModel model)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
if (!string.IsNullOrWhiteSpace(model?.Name))
{
try
{
var check = await _database.GetGallery(model.Name);
if (check == null)
{
return Json(new { success = string.Equals(model?.Name, (await _database.AddGallery(model))?.Name, StringComparison.OrdinalIgnoreCase) });
}
else
{
return Json(new { success = false, message = _localizer["Gallery_Name_Already_Exists"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Add_Gallery"].Value} - {ex?.Message}");
}
}
else
{
return Json(new { success = false, message = _localizer["Name_Cannot_Be_Blank"].Value });
}
}
return Json(new { success = false });
}
[HttpPut]
public async Task<IActionResult> EditGallery(GalleryModel model)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
if (!string.IsNullOrWhiteSpace(model?.Name))
{
try
{
var check = await _database.GetGallery(model.Name);
if (check == null || model.Id == check.Id)
{
var gallery = await _database.GetGallery(model.Id);
if (gallery != null)
{
gallery.Name = model.Name;
gallery.SecretKey = model.SecretKey;
return Json(new { success = string.Equals(model?.Name, (await _database.EditGallery(gallery))?.Name, StringComparison.OrdinalIgnoreCase) });
}
else
{
return Json(new { success = false, message = _localizer["Failed_Edit_Gallery"].Value });
}
}
else
{
return Json(new { success = false, message = _localizer["Gallery_Name_Already_Exists"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Edit_Gallery"].Value} - {ex?.Message}");
}
}
else
{
return Json(new { success = false, message = _localizer["Name_Cannot_Be_Blank"].Value });
}
}
return Json(new { success = false });
}
[HttpDelete]
public async Task<IActionResult> WipeGallery(int id)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var gallery = await _database.GetGallery(id);
if (gallery != null)
{
var galleryDir = Path.Combine(UploadsDirectory, gallery.Name);
if (_fileHelper.DirectoryExists(galleryDir))
{
foreach (var photo in _fileHelper.GetFiles(galleryDir, "*.*", SearchOption.AllDirectories))
{
var thumbnail = Path.Combine(ThumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(photo)}.webp");
_fileHelper.DeleteFileIfExists(thumbnail);
}
_fileHelper.DeleteDirectoryIfExists(galleryDir);
_fileHelper.CreateDirectoryIfNotExists(galleryDir);
if (_config.GetOrDefault("Notifications:Alerts:Destructive_Action", true))
{
await _notificationHelper.Send("Destructive Action Performed", $"The destructive action 'Wipe' was performed on gallery '{gallery.Name}'.", _url.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
}
}
return Json(new { success = await _database.WipeGallery(gallery) });
}
else
{
return Json(new { success = false, message = _localizer["Failed_Wipe_Gallery"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Wipe_Gallery"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpDelete]
public async Task<IActionResult> WipeAllGalleries()
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
if (_fileHelper.DirectoryExists(UploadsDirectory))
{
foreach (var gallery in _fileHelper.GetDirectories(UploadsDirectory, "*", SearchOption.TopDirectoryOnly))
{
_fileHelper.DeleteDirectoryIfExists(gallery);
}
foreach (var thumbnail in _fileHelper.GetFiles(ThumbnailsDirectory, "*.*", SearchOption.AllDirectories))
{
_fileHelper.DeleteFileIfExists(thumbnail);
}
_fileHelper.CreateDirectoryIfNotExists(Path.Combine(UploadsDirectory, "default"));
if (_config.GetOrDefault("Notifications:Alerts:Destructive_Action", true))
{
await _notificationHelper.Send("Destructive Action Performed", $"The destructive action 'Wipe' was performed on all galleries'.", _url.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
}
}
return Json(new { success = await _database.WipeAllGalleries() });
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Wipe_Galleries"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpDelete]
public async Task<IActionResult> DeleteGallery(int id)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var gallery = await _database.GetGallery(id);
if (gallery != null && gallery.Id > 1)
{
var galleryDir = Path.Combine(UploadsDirectory, gallery.Name);
_fileHelper.DeleteDirectoryIfExists(galleryDir);
if (_config.GetOrDefault("Notifications:Alerts:Destructive_Action", true))
{
await _notificationHelper.Send("Destructive Action Performed", $"The destructive action 'Delete' was performed on gallery '{gallery.Name}'.", _url.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
}
return Json(new { success = await _database.DeleteGallery(gallery) });
}
else
{
return Json(new { success = false, message = _localizer["Failed_Delete_Gallery"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Delete_Gallery"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpDelete]
public async Task<IActionResult> DeletePhoto(int id)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var photo = await _database.GetGalleryItem(id);
if (photo != null)
{
var gallery = await _database.GetGallery(photo.GalleryId);
if (gallery != null)
{
var photoPath = Path.Combine(UploadsDirectory, gallery.Name, photo.Title);
_fileHelper.DeleteFileIfExists(photoPath);
return Json(new { success = await _database.DeleteGalleryItem(photo) });
}
}
else
{
return Json(new { success = false, message = _localizer["Failed_Delete_Gallery"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Delete_Gallery"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpDelete]
public async Task<IActionResult> DeleteUser(int id)
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var user = await _database.GetUser(id);
if (user != null && user.Id > 1)
{
if (_config.GetOrDefault("Notifications:Alerts:Destructive_Action", true))
{
await _notificationHelper.Send("Destructive Action Performed", $"The destructive action 'Delete' was performed on user '{user.Username}'.", _url.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
}
return Json(new { success = await _database.DeleteUser(user) });
}
else
{
return Json(new { success = false, message = _localizer["Failed_Delete_User"].Value });
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Delete_User"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
[HttpGet]
public async Task<IActionResult> ExportBackup()
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
var exportDir = Path.Combine(TempDirectory, "Export");
try
{
if (_fileHelper.DirectoryExists(UploadsDirectory))
{
_fileHelper.CreateDirectoryIfNotExists(TempDirectory);
_fileHelper.DeleteDirectoryIfExists(exportDir);
_fileHelper.CreateDirectoryIfNotExists(exportDir);
var dbExport = Path.Combine(exportDir, $"WeddingShare.bak");
var exported = await _database.Export($"Data Source={dbExport}");
if (exported)
{
var uploadsZip = Path.Combine(exportDir, $"Uploads.bak");
ZipFile.CreateFromDirectory(UploadsDirectory, uploadsZip, CompressionLevel.Optimal, false);
var thumbnailsZip = Path.Combine(exportDir, $"Thumbnails.bak");
ZipFile.CreateFromDirectory(ThumbnailsDirectory, thumbnailsZip, CompressionLevel.Optimal, false);
var exportZipFile = Path.Combine(TempDirectory, $"WeddingShare-{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.zip");
_fileHelper.DeleteFileIfExists(exportZipFile);
ZipFile.CreateFromDirectory(exportDir, exportZipFile, CompressionLevel.Optimal, false);
_fileHelper.DeleteFileIfExists(dbExport);
_fileHelper.DeleteFileIfExists(uploadsZip);
_fileHelper.DeleteFileIfExists(thumbnailsZip);
return Json(new { success = true, filename = $"/temp/{Path.GetFileName(exportZipFile)}" });
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Failed_Export"].Value} - {ex?.Message}");
}
finally
{
_fileHelper.DeleteDirectoryIfExists(exportDir);
}
}
return Json(new { success = false });
}
[HttpPost]
public async Task<IActionResult> ImportBackup()
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
var importDir = Path.Combine(TempDirectory, "Import");
try
{
var files = Request?.Form?.Files;
if (files != null && files.Count > 0)
{
foreach (IFormFile file in files)
{
var extension = Path.GetExtension(file.FileName)?.Trim('.');
if (string.Equals("zip", extension, StringComparison.OrdinalIgnoreCase))
{
_fileHelper.CreateDirectoryIfNotExists(TempDirectory);
var filePath = Path.Combine(TempDirectory, "Import.zip");
if (!string.IsNullOrWhiteSpace(filePath))
{
await _fileHelper.SaveFile(file, filePath, FileMode.Create);
_fileHelper.DeleteDirectoryIfExists(importDir);
_fileHelper.CreateDirectoryIfNotExists(importDir);
ZipFile.ExtractToDirectory(filePath, importDir, true);
_fileHelper.DeleteFileIfExists(filePath);
var uploadsZip = Path.Combine(importDir, "Uploads.bak");
ZipFile.ExtractToDirectory(uploadsZip, UploadsDirectory, true);
var thumbnailsZip = Path.Combine(importDir, "Thumbnails.bak");
ZipFile.ExtractToDirectory(thumbnailsZip, ThumbnailsDirectory, true);
var dbImport = Path.Combine(importDir, "WeddingShare.bak");
var imported = await _database.Import($"Data Source={dbImport}");
return Json(new { success = imported });
}
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Import_Failed"].Value} - {ex?.Message}");
}
finally
{
_fileHelper.DeleteDirectoryIfExists(importDir);
}
}
return Json(new { success = false });
}
[HttpPost]
public async Task<IActionResult> RegisterMultifactorAuth(string secret, string code)
{
if (!string.IsNullOrWhiteSpace(secret) && !string.IsNullOrWhiteSpace(code))
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var tfa = new TwoFactorAuth(_config.GetOrDefault("Settings:Title", "WeddingShare"));
if (tfa.VerifyCode(secret, code))
{
var userId = int.Parse(((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1");
if (userId > 0)
{
var set = await _database.SetMultiFactorToken(userId, secret);
if (set)
{
HttpContext.Session.SetString(SessionKey.MultiFactorTokenSet, "true");
return Json(new { success = true });
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["MultiFactor_Token_Set_Failed"].Value} - {ex?.Message}");
}
}
}
return Json(new { success = false });
}
[HttpDelete]
public async Task<IActionResult> ResetMultifactorAuth()
{
if (User?.Identity != null && User.Identity.IsAuthenticated)
{
try
{
var userId = int.Parse(((ClaimsIdentity)User.Identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1");
if (userId > 0)
{
var cleared = await _database.SetMultiFactorToken(userId, string.Empty);
if (cleared)
{
HttpContext.Session.SetString(SessionKey.MultiFactorTokenSet, "false");
return Json(new { success = true });
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["MultiFactor_Token_Set_Failed"].Value} - {ex?.Message}");
}
}
return Json(new { success = false });
}
private async Task<bool> SetUserClaims(HttpContext ctx, UserModel user)
{
try
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Sid, user.Id.ToString()),
new Claim(ClaimTypes.Name, user.Username.ToLower())
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
await ctx.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
return true;
}
catch
{
return false;
}
}
private async Task<bool> FailedLoginDetected(LoginModel model, UserModel user)
{
try
{
if (_config.GetOrDefault("Notifications:Alerts:Failed_Login", true))
{
await _notificationHelper.Send("Invalid Login Detected", $"An invalid login attempt was made for account '{model?.Username}'.", _url.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
}
var failedAttempts = await _database.IncrementLockoutCount(user.Id);
if (failedAttempts >= _config.GetOrDefault("Settings:Account:Lockout_Attempts", 5))
{
var timeout = _config.GetOrDefault("Settings:Account:Lockout_Mins", 60);
await _database.SetLockout(user.Id, DateTime.UtcNow.AddMinutes(timeout));
if (_config.GetOrDefault("Notifications:Alerts:Account_Lockout", true))
{
await _notificationHelper.Send("Account Lockout", $"Account '{model?.Username}' has been locked out for {timeout} minutes due to too many failed login attempts.", _url.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
}
}
return true;
}
catch
{
return false;
}
}
}
}

View File

@@ -1,10 +1,12 @@
using System.IO.Compression;
using System.Net;
using System.Web;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Mysqlx.Expr;
using WeddingShare.Attributes;
using WeddingShare.Constants;
using WeddingShare.Enums;
using WeddingShare.Extensions;
using WeddingShare.Helpers;
@@ -19,10 +21,9 @@ namespace WeddingShare.Controllers
public class GalleryController : Controller
{
private readonly IWebHostEnvironment _hostingEnvironment;
private readonly IConfigHelper _config;
private readonly ISettingsHelper _settings;
private readonly IDatabaseHelper _database;
private readonly IFileHelper _fileHelper;
private readonly IGalleryHelper _gallery;
private readonly IDeviceDetector _deviceDetector;
private readonly IImageHelper _imageHelper;
private readonly INotificationHelper _notificationHelper;
@@ -31,17 +32,17 @@ namespace WeddingShare.Controllers
private readonly ILogger _logger;
private readonly IStringLocalizer<Lang.Translations> _localizer;
private readonly string ImagesDirectory;
private readonly string TempDirectory;
private readonly string UploadsDirectory;
private readonly string ThumbnailsDirectory;
public GalleryController(IWebHostEnvironment hostingEnvironment, IConfigHelper config, IDatabaseHelper database, IFileHelper fileHelper, IGalleryHelper galleryHelper, IDeviceDetector deviceDetector, IImageHelper imageHelper, INotificationHelper notificationHelper, IEncryptionHelper encryptionHelper, Helpers.IUrlHelper urlHelper, ILogger<GalleryController> logger, IStringLocalizer<Lang.Translations> localizer)
public GalleryController(IWebHostEnvironment hostingEnvironment, ISettingsHelper settings, IDatabaseHelper database, IFileHelper fileHelper, IDeviceDetector deviceDetector, IImageHelper imageHelper, INotificationHelper notificationHelper, IEncryptionHelper encryptionHelper, Helpers.IUrlHelper urlHelper, ILogger<GalleryController> logger, IStringLocalizer<Lang.Translations> localizer)
{
_hostingEnvironment = hostingEnvironment;
_config = config;
_settings = settings;
_database = database;
_fileHelper = fileHelper;
_gallery = galleryHelper;
_deviceDetector = deviceDetector;
_imageHelper = imageHelper;
_notificationHelper = notificationHelper;
@@ -50,18 +51,54 @@ namespace WeddingShare.Controllers
_logger = logger;
_localizer = localizer;
TempDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "temp");
UploadsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
ThumbnailsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "thumbnails");
ImagesDirectory = Path.Combine(_hostingEnvironment.WebRootPath, Directories.Images);
TempDirectory = Path.Combine(_hostingEnvironment.WebRootPath, Directories.TempFiles);
UploadsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, Directories.Uploads);
ThumbnailsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, Directories.Thumbnails);
}
[HttpPost]
public IActionResult Login(string id = "default", string? key = null)
public async Task<IActionResult> Login(string? id, string? identifier, string? key = null)
{
int? galleryId = 0;
if (!string.IsNullOrWhiteSpace(identifier))
{
galleryId = await _database.GetGalleryId(identifier);
}
else if (!string.IsNullOrWhiteSpace(id))
{
galleryId = await _database.GetGalleryIdByName(id);
}
GalleryModel? gallery = await _database.GetGallery(galleryId.Value);
if (gallery == null)
{
if (await _settings.GetOrDefault(Settings.Basic.GuestGalleryCreation, false))
{
if (await _database.GetGalleryCount() < await _settings.GetOrDefault(Settings.Basic.MaxGalleryCount, 1000000))
{
gallery = await _database.AddGallery(new GalleryModel()
{
Name = id?.ToLower() ?? GalleryHelper.GenerateGalleryIdentifier(),
SecretKey = key,
Owner = 0
});
}
else
{
return new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.GalleryLimitReached }, false);
}
}
else
{
return new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.GalleryCreationNotAllowed }, false);
}
}
var append = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("id", id)
new KeyValuePair<string, string>("identifier", gallery.Identifier)
};
if (!string.IsNullOrWhiteSpace(key))
@@ -78,76 +115,128 @@ namespace WeddingShare.Controllers
[HttpGet]
[RequiresSecretKey]
[AllowGuestCreate]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> Index(string id = "default", string? key = null, ViewMode? mode = null, GalleryOrder order = GalleryOrder.UploadedDesc, bool partial = false)
public async Task<IActionResult> Index(string? id, string? identifier, string? key = null, ViewMode? mode = null, GalleryGroup group = GalleryGroup.None, GalleryOrder order = GalleryOrder.Descending, GalleryFilter filter = GalleryFilter.All, string? culture = null, bool partial = false)
{
id = (!string.IsNullOrWhiteSpace(id) && !_config.GetOrDefault("Settings:Single_Gallery_Mode", false)) ? id.ToLower() : "default";
int? galleryId = null;
try
if (!string.IsNullOrWhiteSpace(identifier))
{
ViewBag.ViewMode = mode ?? (ViewMode)_gallery.GetConfig(id, "Gallery:Default_View", (int)ViewMode.Default);
galleryId = await _database.GetGalleryId(identifier);
}
catch
else if (!string.IsNullOrWhiteSpace(id))
{
ViewBag.ViewMode = ViewMode.Default;
galleryId = await _database.GetGalleryIdByName(id);
}
var deviceType = HttpContext.Session.GetString(SessionKey.DeviceType);
if (string.IsNullOrWhiteSpace(deviceType))
if (galleryId != null)
{
deviceType = (await _deviceDetector.ParseDeviceType(Request.Headers["User-Agent"].ToString())).ToString();
HttpContext.Session.SetString(SessionKey.DeviceType, deviceType ?? "Desktop");
}
ViewBag.IsMobile = !string.Equals("Desktop", deviceType, StringComparison.OrdinalIgnoreCase);
var galleryPath = Path.Combine(UploadsDirectory, id);
_fileHelper.CreateDirectoryIfNotExists(galleryPath);
_fileHelper.CreateDirectoryIfNotExists(Path.Combine(galleryPath, "Pending"));
GalleryModel? gallery = await _database.GetGallery(id);
if (gallery == null)
{
gallery = await _database.AddGallery(new GalleryModel()
if (!string.IsNullOrWhiteSpace(culture))
{
Name = id.ToLower(),
SecretKey = key
});
}
if (gallery != null)
{
ViewBag.GalleryId = gallery.Name;
var secretKey = await _gallery.GetSecretKey(gallery.Name);
ViewBag.SecretKey = secretKey;
var currentPage = 1;
try
{
currentPage = int.Parse((Request.Query.ContainsKey("page") && !string.IsNullOrWhiteSpace(Request.Query["page"])) ? Request.Query["page"].ToString().ToLower() : "1");
}
catch { }
var mediaType = mode == ViewMode.Slideshow ? MediaType.Image : MediaType.All;
var itemsPerPage = _gallery.GetConfig(gallery?.Name, "Gallery:Items_Per_Page", 50);
var allowedFileTypes = _gallery.GetConfig(gallery?.Name, "Gallery:Allowed_File_Types", ".jpg,.jpeg,.png,.mp4,.mov").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var items = (await _database.GetAllGalleryItems(gallery?.Id, GalleryItemState.Approved, mediaType, order, itemsPerPage, currentPage))?.Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x.Title).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
var isAdmin = User?.Identity != null && User.Identity.IsAuthenticated;
FileUploader? fileUploader = null;
if (!string.Equals("All", gallery?.Name, StringComparison.OrdinalIgnoreCase) && (_gallery.GetConfig(gallery?.Name, "Gallery:Upload", true) || isAdmin))
{
var uploadActvated = isAdmin;
try
{
if (!uploadActvated)
{
var periods = _gallery.GetConfig(gallery?.Name, "Gallery:Upload_Period", "1970-01-01 00:00")?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
HttpContext.Session.SetString(SessionKey.SelectedLanguage, culture);
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
}
catch { }
}
try
{
ViewBag.ViewMode = mode ?? (ViewMode)await _settings.GetOrDefault(Settings.Gallery.DefaultView, (int)ViewMode.Default, galleryId);
}
catch
{
ViewBag.ViewMode = ViewMode.Default;
}
var deviceType = HttpContext.Session.GetString(SessionKey.DeviceType);
if (string.IsNullOrWhiteSpace(deviceType))
{
deviceType = (await _deviceDetector.ParseDeviceType(Request.Headers["User-Agent"].ToString())).ToString();
HttpContext.Session.SetString(SessionKey.DeviceType, deviceType ?? "Desktop");
}
ViewBag.IsMobile = !string.Equals("Desktop", deviceType, StringComparison.OrdinalIgnoreCase);
GalleryModel? gallery = await _database.GetGallery(galleryId.Value);
if (gallery != null)
{
var galleryPath = Path.Combine(UploadsDirectory, gallery.Identifier);
_fileHelper.CreateDirectoryIfNotExists(galleryPath);
_fileHelper.CreateDirectoryIfNotExists(Path.Combine(galleryPath, "Pending"));
ViewBag.GalleryIdentifier = gallery.Identifier;
var secretKey = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, gallery.Id);
ViewBag.SecretKey = secretKey;
var currentPage = 1;
try
{
currentPage = int.Parse((Request.Query.ContainsKey("page") && !string.IsNullOrWhiteSpace(Request.Query["page"])) ? Request.Query["page"].ToString().ToLower() : "1");
}
catch { }
var mediaType = MediaType.All;
if (mode == ViewMode.Slideshow)
{
mediaType = MediaType.Image;
}
else
{
switch (filter)
{
case GalleryFilter.Images:
mediaType = MediaType.Image;
break;
case GalleryFilter.Videos:
mediaType = MediaType.Video;
break;
default:
mediaType = MediaType.All;
break;
}
}
var orientation = ImageOrientation.None;
switch (filter)
{
case GalleryFilter.Landscape:
orientation = ImageOrientation.Landscape;
break;
case GalleryFilter.Portrait:
orientation = ImageOrientation.Portrait;
break;
case GalleryFilter.Square:
orientation = ImageOrientation.Square;
break;
default:
orientation = ImageOrientation.None;
break;
}
var itemsPerPage = await _settings.GetOrDefault(Settings.Gallery.ItemsPerPage, 50, gallery?.Id);
var allowedFileTypes = (await _settings.GetOrDefault(Settings.Gallery.AllowedFileTypes, ".jpg,.jpeg,.png,.mp4,.mov", gallery?.Id)).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var items = (await _database.GetAllGalleryItems(gallery?.Id, GalleryItemState.Approved, mediaType, orientation, group, order, itemsPerPage, currentPage))?.Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x.Title).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
var userPermissions = User?.Identity?.GetUserPermissions() ?? AccessPermissions.None;
var isGalleryAdmin = User?.Identity != null && User.Identity.IsAuthenticated && userPermissions.HasFlag(AccessPermissions.Gallery_Upload);
var uploadActvated = !gallery.Identifier.Equals("All", StringComparison.OrdinalIgnoreCase) && (await _settings.GetOrDefault(Settings.Gallery.Upload, true, gallery?.Id) || isGalleryAdmin);
if (uploadActvated)
{
try
{
var periods = (await _settings.GetOrDefault(Settings.Gallery.UploadPeriod, "1970-01-01 00:00", gallery?.Id))?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (periods != null)
{
{
uploadActvated = false;
var now = DateTime.UtcNow;
foreach (var period in periods)
{
@@ -174,47 +263,46 @@ namespace WeddingShare.Controllers
}
}
}
catch
{
uploadActvated = true;
}
}
catch
var itemCounts = await _database.GetGalleryItemCount(gallery?.Id, GalleryItemState.All, mediaType, orientation);
var model = new PhotoGallery()
{
uploadActvated = true;
}
Gallery = gallery,
SecretKey = secretKey,
Images = items?.Select(x => new PhotoGalleryImage()
{
Id = x.Id,
GalleryId = x.GalleryId,
Name = Path.GetFileName(x.Title),
UploadedBy = x.UploadedBy,
UploadDate = x.UploadedDate,
ImagePath = $"/{Path.Combine(UploadsDirectory, gallery.Identifier).Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{x.Title}",
ThumbnailPath = $"/{Path.Combine(ThumbnailsDirectory, gallery.Identifier).Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{Path.GetFileNameWithoutExtension(x.Title)}.webp",
ThumbnailPathFallback = $"/{ThumbnailsDirectory.Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{Path.GetFileNameWithoutExtension(x.Title)}.webp",
MediaType = x.MediaType
})?.ToList(),
CurrentPage = currentPage,
ApprovedCount = (int)itemCounts["Approved"],
PendingCount = (int)itemCounts["Pending"],
ItemsPerPage = itemsPerPage,
UploadActivated = uploadActvated,
ViewMode = (ViewMode)ViewBag.ViewMode,
GroupBy = group,
OrderBy = order,
Pagination = order != GalleryOrder.Random,
LoadScripts = !partial
};
if (uploadActvated)
{
fileUploader = new FileUploader(gallery?.Name ?? "default", secretKey, "/Gallery/UploadImage", _config.GetOrDefault("Settings:Identity_Check:Require_Identity_For_Upload", false));
}
return partial ? PartialView("~/Views/Gallery/GalleryWrapper.cshtml", model) : View(model);
}
var model = new PhotoGallery()
{
GalleryId = gallery?.Id,
GalleryName = gallery?.Name,
Images = items?.Select(x => new PhotoGalleryImage()
{
Id = x.Id,
GalleryId = x.GalleryId,
GalleryName = x.GalleryName,
Name = Path.GetFileName(x.Title),
UploadedBy = x.UploadedBy,
ImagePath = $"/{Path.Combine(UploadsDirectory, x.GalleryName).Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{x.Title}",
ThumbnailPath = $"/{ThumbnailsDirectory.Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{Path.GetFileNameWithoutExtension(x.Title)}.webp",
MediaType = x.MediaType
})?.ToList(),
CurrentPage = currentPage,
ApprovedCount = gallery?.ApprovedItems ?? 0,
PendingCount = gallery?.PendingItems ?? 0,
ItemsPerPage = itemsPerPage,
FileUploader = fileUploader,
ViewMode = (ViewMode)ViewBag.ViewMode,
Pagination = order != GalleryOrder.Random,
LoadScripts = !partial
};
return partial ? PartialView("~/Views/Gallery/GalleryWrapper.cshtml", model) : View(model);
}
return View(new PhotoGallery());
return new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.InvalidGalleryId }, false);
}
[HttpPost]
@@ -224,8 +312,7 @@ namespace WeddingShare.Controllers
try
{
string galleryId = (Request?.Form?.FirstOrDefault(x => string.Equals("Id", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty;
if (string.IsNullOrWhiteSpace(galleryId))
if (!int.TryParse((Request?.Form?.FirstOrDefault(x => string.Equals("Id", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty, out var galleryId))
{
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Gallery_Id"].Value } });
}
@@ -233,19 +320,19 @@ namespace WeddingShare.Controllers
var gallery = await _database.GetGallery(galleryId);
if (gallery != null)
{
var secretKey = await _gallery.GetSecretKey(galleryId);
var secretKey = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, gallery.Id);
string key = (Request?.Form?.FirstOrDefault(x => string.Equals("SecretKey", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString() ?? string.Empty;
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
{
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Secret_Key_Warning"].Value } });
}
string uploadedBy = HttpContext.Session.GetString(SessionKey.ViewerIdentity) ?? "Anonymous";
string uploadedBy = HttpContext.Session.GetString(SessionKey.ViewerIdentity)?.Trim() ?? "Anonymous";
var files = Request?.Form?.Files;
if (files != null && files.Count > 0)
{
var requiresReview = _gallery.GetConfig(galleryId, "Gallery:Require_Review", true);
var requiresReview = await _settings.GetOrDefault(Settings.Gallery.RequireReview, true, gallery.Id);
var uploaded = 0;
var errors = new List<string>();
@@ -254,11 +341,11 @@ namespace WeddingShare.Controllers
try
{
var extension = Path.GetExtension(file.FileName);
var maxGallerySize = _gallery.GetConfig(galleryId, "Gallery:Max_Size_Mb", 1024L) * 1000000;
var maxFilesSize = _gallery.GetConfig(galleryId, "Gallery:Max_File_Size_Mb", 10L) * 1000000;
var galleryPath = Path.Combine(UploadsDirectory, gallery.Name);
var maxGallerySize = await _settings.GetOrDefault(Settings.Gallery.MaxSizeMB, 1024L, gallery.Id) * 1000000;
var maxFilesSize = await _settings.GetOrDefault(Settings.Gallery.MaxFileSizeMB, 50L, gallery.Id) * 1000000;
var galleryPath = Path.Combine(UploadsDirectory, gallery.Identifier);
var allowedFileTypes = _gallery.GetConfig(galleryId, "Gallery:Allowed_File_Types", ".jpg,.jpeg,.png,.mp4,.mov").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
var allowedFileTypes = (await _settings.GetOrDefault(Settings.Gallery.AllowedFileTypes, ".jpg,.jpeg,.png,.mp4,.mov", gallery.Id)).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (!allowedFileTypes.Any(x => string.Equals(x.Trim('.'), extension.Trim('.'), StringComparison.OrdinalIgnoreCase)))
{
errors.Add($"{_localizer["File_Upload_Failed"].Value}. {_localizer["Invalid_File_Type"].Value}");
@@ -273,7 +360,7 @@ namespace WeddingShare.Controllers
}
else
{
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
var fileName = _fileHelper.SanitizeFilename($"{(!string.IsNullOrWhiteSpace(uploadedBy) ? $"{uploadedBy.Replace(" ", "_")}-" : string.Empty)}{Guid.NewGuid()}{Path.GetExtension(file.FileName)}");
galleryPath = requiresReview ? Path.Combine(galleryPath, "Pending") : galleryPath;
_fileHelper.CreateDirectoryIfNotExists(galleryPath);
@@ -281,27 +368,43 @@ namespace WeddingShare.Controllers
var filePath = Path.Combine(galleryPath, fileName);
if (!string.IsNullOrWhiteSpace(filePath))
{
await _fileHelper.SaveFile(file, filePath, FileMode.Create);
var isDemoMode = await _settings.GetOrDefault(Settings.IsDemoMode, false);
if (!isDemoMode)
{
await _fileHelper.SaveFile(file, filePath, FileMode.Create);
}
else
{
System.IO.File.Copy(Path.Combine(ImagesDirectory, $"DemoImage.png"), filePath, true);
}
var checksum = await _fileHelper.GetChecksum(filePath);
if (_gallery.GetConfig(galleryId, "Gallery:Prevent_Duplicates", true) && (string.IsNullOrWhiteSpace(checksum) || await _database.GetGalleryItemByChecksum(gallery.Id, checksum) != null))
if (await _settings.GetOrDefault(Settings.Gallery.PreventDuplicates, true, gallery.Id) && (string.IsNullOrWhiteSpace(checksum) || await _database.GetGalleryItemByChecksum(gallery.Id, checksum) != null))
{
errors.Add($"{_localizer["File_Upload_Failed"].Value}. {_localizer["Duplicate_Item_Detected"].Value}");
_fileHelper.DeleteFileIfExists(filePath);
}
else
{
_fileHelper.CreateDirectoryIfNotExists(ThumbnailsDirectory);
await _imageHelper.GenerateThumbnail(filePath, Path.Combine(ThumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(filePath)}.webp"), _config.GetOrDefault("Settings:Thumbnail_Size", 720));
{
var gallerySavePath = Path.Combine(ThumbnailsDirectory, gallery.Identifier);
_fileHelper.CreateDirectoryIfNotExists(ThumbnailsDirectory);
_fileHelper.CreateDirectoryIfNotExists(gallerySavePath);
var savePath = Path.Combine(gallerySavePath, $"{Path.GetFileNameWithoutExtension(filePath)}.webp");
await _imageHelper.GenerateThumbnail(filePath, savePath, await _settings.GetOrDefault(Settings.Basic.ThumbnailSize, 720));
var item = await _database.AddGalleryItem(new GalleryItemModel()
{
GalleryId = gallery.Id,
Title = fileName,
UploadedBy = uploadedBy,
UploadedDate = await _fileHelper.GetCreationDatetime(filePath),
Checksum = checksum,
MediaType = _imageHelper.GetMediaType(filePath),
State = requiresReview ? GalleryItemState.Pending : GalleryItemState.Approved
Orientation = await _imageHelper.GetOrientation(savePath),
State = requiresReview ? GalleryItemState.Pending : GalleryItemState.Approved,
FileSize = file.Length,
});
if (item?.Id > 0)
@@ -347,8 +450,7 @@ namespace WeddingShare.Controllers
try
{
string galleryId = (Request?.Form?.FirstOrDefault(x => string.Equals("Id", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty;
if (string.IsNullOrWhiteSpace(galleryId))
if (!int.TryParse((Request?.Form?.FirstOrDefault(x => string.Equals("Id", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty, out var galleryId))
{
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Gallery_Id"].Value } });
}
@@ -356,7 +458,7 @@ namespace WeddingShare.Controllers
var gallery = await _database.GetGallery(galleryId);
if (gallery != null)
{
var secretKey = await _gallery.GetSecretKey(galleryId);
var secretKey = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, galleryId);
string key = (Request?.Form?.FirstOrDefault(x => string.Equals("SecretKey", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString() ?? string.Empty;
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
{
@@ -364,12 +466,12 @@ namespace WeddingShare.Controllers
}
var uploadedBy = HttpContext.Session.GetString(SessionKey.ViewerIdentity) ?? "Anonymous";
var requiresReview = _gallery.GetConfig(galleryId, "Gallery:Require_Review", true);
var requiresReview = await _settings.GetOrDefault(Settings.Gallery.RequireReview, true, galleryId);
int uploaded = int.Parse((Request?.Form?.FirstOrDefault(x => string.Equals("Count", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString() ?? "0");
if (uploaded > 0 && requiresReview && _config.GetOrDefault("Notifications:Alerts:Pending_Review", true))
if (uploaded > 0 && requiresReview && await _settings.GetOrDefault(Notifications.Alerts.PendingReview, true))
{
await _notificationHelper.Send(_localizer["New_Items_Pending_Review"].Value, $"{uploaded} new item(s) have been uploaded to gallery '{gallery.Name}' by '{(!string.IsNullOrWhiteSpace(uploadedBy) ? uploadedBy : "Anonymous")}' and are awaiting your review.", _urlHelper.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
await _notificationHelper.Send(_localizer["New_Items_Pending_Review"].Value, $"{uploaded} new item(s) have been uploaded to gallery '{gallery.Name}' by '{(!string.IsNullOrWhiteSpace(uploadedBy) ? uploadedBy : "Anonymous")}' and are awaiting your review.", _urlHelper.GenerateBaseUrl(HttpContext?.Request, "/Account"));
}
Response.StatusCode = (int)HttpStatusCode.OK;
@@ -390,21 +492,84 @@ namespace WeddingShare.Controllers
}
[HttpPost]
public async Task<IActionResult> DownloadGallery(int id)
public async Task<IActionResult> DownloadGallery(int id, string? secretKey, string? group)
{
try
{
var gallery = await _database.GetGallery(id);
if (gallery != null)
{
if (_gallery.GetConfig(gallery.Name, "Gallery:Download", true) || (User?.Identity != null && User.Identity.IsAuthenticated))
secretKey = secretKey ?? string.Empty;
var gallerySecret = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, gallery.Id);
if (!secretKey.Equals(gallerySecret))
{
var galleryDir = id > 0 ? Path.Combine(UploadsDirectory, gallery.Name) : UploadsDirectory;
return Json(new { success = false, message = _localizer["Failed_Download_Gallery_Invalid_Key"].Value });
}
if (await _settings.GetOrDefault(Settings.Gallery.Download, true, gallery?.Id) || (User?.Identity != null && User.Identity.IsAuthenticated))
{
var galleryDir = id > 0 ? Path.Combine(UploadsDirectory, gallery.Identifier) : UploadsDirectory;
if (_fileHelper.DirectoryExists(galleryDir))
{
var keepFiles = new List<string>();
if (!string.IsNullOrWhiteSpace(group))
{
var groupParts = group.Split('|', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (groupParts != null && groupParts.Length == 2)
{
var items = await _database.GetAllGalleryItems(id, GalleryItemState.Approved);
foreach (GalleryGroup type in Enum.GetValues(typeof(GalleryGroup)))
{
if (((int)type).ToString().Equals(groupParts[0]))
{
try
{
IEnumerable<IGrouping<string, GalleryItemModel>>? filtered = null;
switch (type)
{
case GalleryGroup.Date:
filtered = items?.GroupBy(x => x.UploadedDate != null ? x.UploadedDate.Value.ToString("dddd, d MMMM yyyy") : "Unknown");
break;
case GalleryGroup.MediaType:
filtered = items?.GroupBy(x => x.MediaType.ToString());
break;
case GalleryGroup.Uploader:
filtered = items?.GroupBy(x => x.UploadedBy ?? "Anonymous");
break;
}
if (filtered != null)
{
foreach (var f in filtered)
{
if (f.Key.Equals(groupParts[1]))
{
if (f.Any())
{
keepFiles.AddRange(f.Select(x => x.Title));
}
break;
}
}
}
}
catch { }
break;
}
}
}
else
{
return Json(new { success = false, message = _localizer["Failed_Download_Gallery"].Value });
}
}
_fileHelper.CreateDirectoryIfNotExists(TempDirectory);
var tempZipFile = Path.Combine(TempDirectory, $"{gallery.Name}-{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.zip");
var tempZipFile = Path.Combine(TempDirectory, $"{gallery.Identifier}-{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.zip");
ZipFile.CreateFromDirectory(galleryDir, tempZipFile, CompressionLevel.Optimal, false);
if (User?.Identity == null || !User.Identity.IsAuthenticated)
@@ -416,6 +581,14 @@ namespace WeddingShare.Controllers
{
entry.Delete();
}
if (keepFiles.Any())
{
foreach (var entry in archive.Entries.Where(x => !keepFiles.Exists(y => Path.GetFileName(y).Equals(x.Name))).ToList())
{
entry.Delete();
}
}
}
}

View File

@@ -2,7 +2,10 @@ using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using WeddingShare.Constants;
using WeddingShare.Extensions;
using WeddingShare.Helpers;
using WeddingShare.Helpers.Database;
using WeddingShare.Models;
namespace WeddingShare.Controllers
@@ -10,24 +13,29 @@ namespace WeddingShare.Controllers
[AllowAnonymous]
public class HomeController : Controller
{
private readonly IConfigHelper _config;
private readonly IGalleryHelper _gallery;
private readonly ISettingsHelper _settings;
private readonly IDatabaseHelper _database;
private readonly IDeviceDetector _deviceDetector;
private readonly IAuditHelper _audit;
private readonly ILogger _logger;
private readonly IStringLocalizer<Lang.Translations> _localizer;
public HomeController(IConfigHelper config, IGalleryHelper gallery, IDeviceDetector deviceDetector, ILogger<HomeController> logger, IStringLocalizer<Lang.Translations> localizer)
public HomeController(ISettingsHelper settings, IDatabaseHelper database, IDeviceDetector deviceDetector, IAuditHelper audit, ILogger<HomeController> logger, IStringLocalizer<Lang.Translations> localizer)
{
_config = config;
_gallery = gallery;
_settings = settings;
_database = database;
_deviceDetector = deviceDetector;
_audit = audit;
_logger = logger;
_localizer = localizer;
}
[HttpGet]
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public async Task<IActionResult> Index()
{
var model = new Views.Home.IndexModel();
try
{
var deviceType = HttpContext.Session.GetString(SessionKey.DeviceType);
@@ -37,21 +45,39 @@ namespace WeddingShare.Controllers
HttpContext.Session.SetString(SessionKey.DeviceType, deviceType ?? "Desktop");
}
if (_config.GetOrDefault("Settings:Single_Gallery_Mode", false))
if (await _settings.GetOrDefault(Settings.Basic.SingleGalleryMode, false))
{
var key = await _gallery.GetSecretKey("default");
var key = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, 1);
if (string.IsNullOrWhiteSpace(key))
{
return RedirectToAction("Index", "Gallery");
return RedirectToAction("Index", "Gallery", new { identifier = "default" });
}
}
model.GalleryNames = await _settings.GetOrDefault(Settings.GallerySelector.Dropdown, false) ? (await _database.GetGalleryNames()).Where(x => !x.Equals("all", StringComparison.OrdinalIgnoreCase)) : new List<string>();
if (await _settings.GetOrDefault(Settings.GallerySelector.HideDefaultOption, false))
{
model.GalleryNames = model.GalleryNames.Where(x => !x.Equals("default", StringComparison.OrdinalIgnoreCase));
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Homepage_Load_Error"].Value} - {ex?.Message}");
}
return View();
return View(model);
}
[HttpGet]
[Route("CookiePolicy")]
[Route("Home/CookiePolicy")]
public async Task<IActionResult> CookiePolicy()
{
ViewBag.CompanyName = await _settings.GetOrDefault(Settings.Basic.Title, "WeddingShare");
ViewBag.SiteHostname = await _settings.GetOrDefault(Settings.Basic.BaseUrl, "www.wedding-share.org");
ViewBag.CustomPolicy = await _settings.GetOrDefault(Settings.Policies.CookiePolicy, string.Empty);
return View("~/Views/Home/CookiePolicy.cshtml");
}
[HttpPost]
@@ -59,7 +85,11 @@ namespace WeddingShare.Controllers
{
try
{
if (Regex.IsMatch(name, @"^[a-zA-Z-\s\-\']+$", RegexOptions.Compiled))
if (HtmlSanitizer.MayContainXss(name))
{
return Json(new { success = false, reason = 1 });
}
else
{
HttpContext.Session.SetString(SessionKey.ViewerIdentity, name);
@@ -73,5 +103,22 @@ namespace WeddingShare.Controllers
return Json(new { success = false });
}
[HttpPost]
public async Task<IActionResult> LogCookieApproval()
{
try
{
var ipAddress = Request.HttpContext.TryGetIpAddress();
return Json(new { success = await _audit.LogAction("Visitor", $"{_localizer["Audit_CookieConsentApproved"].Value}: {ipAddress}") });
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Cookie_Audit_Error"].Value}");
}
return Json(new { success = false });
}
}
}

View File

@@ -0,0 +1,80 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.Models;
namespace WeddingShare.Controllers
{
[AllowAnonymous]
public class LanguageController : Controller
{
private readonly ISettingsHelper _settings;
private readonly ILanguageHelper _languageHelper;
private readonly ILogger<LanguageController> _logger;
public LanguageController(ISettingsHelper settings, ILanguageHelper languageHelper, ILogger<LanguageController> logger)
{
_settings = settings;
_languageHelper = languageHelper;
_logger = logger;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var options = new List<SupportedLanguage>();
try
{
var defaultLang = HttpContext.Session.GetString(SessionKey.SelectedLanguage);
if (string.IsNullOrWhiteSpace(defaultLang))
{
defaultLang = await _languageHelper.GetOrFallbackCulture(string.Empty, await _settings.GetOrDefault(Settings.Languages.Default, "en-GB"));
}
options = (await _languageHelper.DetectSupportedCulturesAsync())
.Select(x => new SupportedLanguage() { Key = x.Name, Value = $"{(x.EnglishName.Contains("(") ? x.EnglishName.Substring(0, x.EnglishName.IndexOf("(")) : x.EnglishName).Trim()} ({x.Name})", Selected = string.Equals(defaultLang, x.Name, StringComparison.OrdinalIgnoreCase) })
.OrderBy(x => x.Value.ToLower())
.ToList();
}
catch { }
return Json(new { supported = options });
}
[HttpPost]
public async Task<IActionResult> ChangeDisplayLanguage(string culture)
{
try
{
culture = await _languageHelper.GetOrFallbackCulture(culture, await _settings.GetOrDefault(Settings.Languages.Default, "en-GB"));
HttpContext.Session.SetString(SessionKey.SelectedLanguage, culture);
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
return Json(new { success = true });
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to set display language to '{culture}' - {ex?.Message}");
culture = "en-GB";
HttpContext.Session.SetString(SessionKey.SelectedLanguage, culture);
Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
);
}
return Json(new { success = false });
}
}
}

View File

@@ -0,0 +1,45 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using WeddingShare.Constants;
using WeddingShare.Helpers;
using WeddingShare.Models;
namespace WeddingShare.Controllers
{
[AllowAnonymous]
public class SponsorsController : Controller
{
private readonly ISettingsHelper _settings;
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _logger;
private readonly IStringLocalizer<Lang.Translations> _localizer;
public SponsorsController(ISettingsHelper settings, IHttpClientFactory clientFactory, ILogger<HomeController> logger, IStringLocalizer<Lang.Translations> localizer)
{
_settings = settings;
_clientFactory = clientFactory;
_logger = logger;
_localizer = localizer;
}
[HttpGet]
public async Task<IActionResult> Index()
{
var model = new Views.Sponsors.IndexModel();
try
{
var client = _clientFactory.CreateClient("SponsorsClient");
var endpoint = await _settings.GetOrDefault(Sponsors.Endpoint, "/sponsors.json");
model.SponsorsList = await client.GetFromJsonAsync<SponsorsList>(endpoint);
}
catch (Exception ex)
{
_logger.LogError(ex, $"{_localizer["Sponsors_Load_Error"].Value} - {ex?.Message}");
}
return PartialView(model);
}
}
}

View File

@@ -0,0 +1,47 @@
namespace WeddingShare.Enums
{
[Flags]
public enum AccessPermissions
{
None = 0,
Login = 1,
Review_View = 2,
Review_Approve = 4,
Review_Reject = 8,
Review_Delete = 16,
Gallery_View = 32,
Gallery_Create = 64,
Gallery_Update = 128,
Gallery_Delete = 256,
Gallery_Upload = 1073741824,
Gallery_Download = 512,
Gallery_Wipe = 1024,
User_View = 2048,
User_Create = 4096,
User_Update = 8192,
User_Delete = 16384,
User_Change_Password = 32768,
User_Reset_MFA = 65536,
User_Freeze = 131072,
CustomResource_View = 262144,
CustomResource_Create = 524288,
CustomResource_Update = 1048576,
CustomResource_Delete = 2097152,
Settings_View = 4194304,
Settings_Update = 8388608,
Settings_Gallery_Update = 16777216,
Audit_View = 33554432,
Data_View = 67108864,
Data_Import = 134217728,
Data_Export = 268435456,
Data_Wipe = 536870912
}
}

View File

@@ -0,0 +1,8 @@
namespace WeddingShare.Enums
{
public enum AccountState
{
Active = 0,
Frozen = 1
}
}

View File

@@ -0,0 +1,13 @@
namespace WeddingShare.Enums
{
public enum AccountTabs
{
Reviews,
Galleries,
Users,
Resources,
Settings,
Audit,
Data
}
}

View File

@@ -0,0 +1,12 @@
namespace WeddingShare.Enums
{
public enum GalleryFilter
{
All,
Images,
Videos,
Landscape,
Portrait,
Square
}
}

View File

@@ -0,0 +1,10 @@
namespace WeddingShare.Enums
{
public enum GalleryGroup
{
None,
Date,
MediaType,
Uploader
}
}

View File

@@ -2,11 +2,8 @@
{
public enum GalleryOrder
{
None,
UploadedAsc,
UploadedDesc,
NameAsc,
NameDesc,
Ascending,
Descending,
Random
}
}

View File

@@ -0,0 +1,8 @@
namespace WeddingShare.Enums
{
public enum Themes
{
Default = 1,
Dark = 2
}
}

View File

@@ -0,0 +1,11 @@
namespace WeddingShare.Enums
{
public enum UserLevel
{
Basic = 0,
Reviewer = 1,
Moderator = 2,
Admin = 3,
Owner = 4
}
}

View File

@@ -0,0 +1,19 @@
namespace WeddingShare.Extensions
{
public static class DictionaryExtensions
{
public static string GetValue(this IDictionary<string, string> value, string key, string defaultValue = "")
{
try
{
if (value.ContainsKey(key))
{
return value[key];
}
}
catch { }
return defaultValue;
}
}
}

View File

@@ -0,0 +1,50 @@
namespace WeddingShare.Extensions
{
public static class HttpContextExtensions
{
public static string TryGetIpAddress(this HttpContext ctx)
{
try
{
var ipAddress = TryGetHeaderValue(ctx, ["CF-Connecting-IP", "CF-Connecting-IPv6", "X-Forwarded-For", "HTTP_X_FORWARDED_FOR", "REMOTE_ADDR"]);
if (string.IsNullOrWhiteSpace(ipAddress) || ipAddress.Equals("Unknown", StringComparison.OrdinalIgnoreCase))
{
return ctx.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
}
return ipAddress;
}
catch
{
return "Unknown";
}
}
public static string TryGetCountry(this HttpContext ctx)
{
return ctx.TryGetHeaderValue(["CF-IPCountry"]);
}
public static string TryGetHeaderValue(this HttpContext ctx, string[] headers)
{
foreach (var header in headers)
{
try
{
string? val = ctx.Request.Headers[header];
if (!string.IsNullOrWhiteSpace(val))
{
var vals = val.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (vals.Length != 0)
{
return vals[0];
}
}
}
catch { }
}
return "Unknown";
}
}
}

View File

@@ -0,0 +1,141 @@
using System.Security.Claims;
using System.Security.Principal;
using WeddingShare.Enums;
namespace WeddingShare.Extensions
{
public static class UserClaimsExtentions
{
public static int GetUserId(this IIdentity identity)
{
try
{
return int.Parse(((ClaimsIdentity)identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Sid, x.Type, StringComparison.OrdinalIgnoreCase))?.Value ?? "-1");
}
catch { }
return -1;
}
public static UserLevel GetUserLevel(this IIdentity identity)
{
try
{
var level = ((ClaimsIdentity)identity).Claims.FirstOrDefault(x => string.Equals(ClaimTypes.Role, x.Type, StringComparison.OrdinalIgnoreCase))?.Value;
foreach (UserLevel l in Enum.GetValues(typeof(UserLevel)))
{
if (l.ToString().Equals(level, StringComparison.OrdinalIgnoreCase))
{
return l;
}
}
}
catch { }
return UserLevel.Basic;
}
public static AccessPermissions GetUserPermissions(this IIdentity identity)
{
try
{
var level = identity.GetUserLevel();
switch (level)
{
case UserLevel.Basic:
return AccessPermissions.Login;
case UserLevel.Reviewer:
return
AccessPermissions.Login
| AccessPermissions.Review_View
| AccessPermissions.Review_Approve
| AccessPermissions.Review_Reject
| AccessPermissions.Review_Delete
| AccessPermissions.Gallery_View;
case UserLevel.Moderator:
return
AccessPermissions.Login
| AccessPermissions.Review_View
| AccessPermissions.Review_Approve
| AccessPermissions.Review_Reject
| AccessPermissions.Review_Delete
| AccessPermissions.Gallery_View
| AccessPermissions.Gallery_Update
| AccessPermissions.Gallery_Upload
| AccessPermissions.Gallery_Download
| AccessPermissions.User_View
| AccessPermissions.User_Reset_MFA
| AccessPermissions.User_Freeze
| AccessPermissions.CustomResource_View
| AccessPermissions.Audit_View;
case UserLevel.Admin:
return
AccessPermissions.Login
| AccessPermissions.Review_View
| AccessPermissions.Review_Approve
| AccessPermissions.Review_Reject
| AccessPermissions.Review_Delete
| AccessPermissions.Gallery_View
| AccessPermissions.Gallery_Create
| AccessPermissions.Gallery_Update
| AccessPermissions.Gallery_Delete
| AccessPermissions.Gallery_Upload
| AccessPermissions.Gallery_Download
| AccessPermissions.User_View
| AccessPermissions.User_Create
| AccessPermissions.User_Update
| AccessPermissions.User_Change_Password
| AccessPermissions.User_Reset_MFA
| AccessPermissions.User_Freeze
| AccessPermissions.CustomResource_View
| AccessPermissions.CustomResource_Create
| AccessPermissions.CustomResource_Update
| AccessPermissions.CustomResource_Delete
| AccessPermissions.Settings_View
| AccessPermissions.Settings_Update
| AccessPermissions.Settings_Gallery_Update
| AccessPermissions.Audit_View;
case UserLevel.Owner:
return
AccessPermissions.Login
| AccessPermissions.Review_View
| AccessPermissions.Review_Approve
| AccessPermissions.Review_Reject
| AccessPermissions.Review_Delete
| AccessPermissions.Gallery_View
| AccessPermissions.Gallery_Create
| AccessPermissions.Gallery_Update
| AccessPermissions.Gallery_Delete
| AccessPermissions.Gallery_Upload
| AccessPermissions.Gallery_Download
| AccessPermissions.Gallery_Wipe
| AccessPermissions.User_View
| AccessPermissions.User_Create
| AccessPermissions.User_Update
| AccessPermissions.User_Delete
| AccessPermissions.User_Change_Password
| AccessPermissions.User_Reset_MFA
| AccessPermissions.User_Freeze
| AccessPermissions.CustomResource_View
| AccessPermissions.CustomResource_Create
| AccessPermissions.CustomResource_Update
| AccessPermissions.CustomResource_Delete
| AccessPermissions.Settings_View
| AccessPermissions.Settings_Update
| AccessPermissions.Settings_Gallery_Update
| AccessPermissions.Audit_View
| AccessPermissions.Data_View
| AccessPermissions.Data_Import
| AccessPermissions.Data_Export
| AccessPermissions.Data_Wipe;
default:
return AccessPermissions.None;
}
}
catch { }
return AccessPermissions.None;
}
}
}

View File

@@ -0,0 +1,43 @@
using WeddingShare.Helpers.Database;
using WeddingShare.Models.Database;
namespace WeddingShare.Helpers
{
public interface IAuditHelper
{
Task<bool> LogAction(string? user, string? action);
}
public class AuditHelper : IAuditHelper
{
private readonly IDatabaseHelper _databaseHelper;
private readonly ILogger _logger;
public AuditHelper(IDatabaseHelper databaseHelper, ILogger<AuditHelper> logger)
{
_databaseHelper = databaseHelper;
_logger = logger;
}
public async Task<bool> LogAction(string? user, string? action)
{
if (!string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(action))
{
try
{
return await _databaseHelper.AddAuditLog(new AuditLogModel()
{
Username = user,
Message = action
}) != null;
}
catch (Exception ex)
{
_logger.LogError($"Failed to log audit message '{action}' for user '{user}'", ex);
}
}
return false;
}
}
}

View File

@@ -1,7 +1,4 @@
using WeddingShare.Helpers.Migrators;
using WeddingShare.Models.Migrator;
namespace WeddingShare.Helpers
namespace WeddingShare.Helpers
{
public interface IConfigHelper
{
@@ -34,23 +31,21 @@ namespace WeddingShare.Helpers
{
try
{
foreach (var envKey in KeyHelper.GetAlternateVersions(key, galleryId))
var envKey = !string.IsNullOrWhiteSpace(galleryId) ? $"{key}_{galleryId}" : key;
if (!this.IsProtectedVariable(envKey))
{
if (!this.IsProtectedVariable(envKey.Key))
{
var keyName = string.Join('_', envKey.Key.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Skip(1)).Trim('_').ToUpper();
var keyName = string.Join('_', envKey.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Skip(1)).Trim('_').ToUpper();
var value = _environment.GetEnvironmentVariable(keyName);
if (!string.IsNullOrWhiteSpace(value))
{
return envKey.MigrationAction != null ? envKey.MigrationAction(value) : value;
}
var value = _environment.GetEnvironmentVariable(keyName);
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to get environment variable '{key}'");
_logger.LogWarning(ex, $"Failed to get environment variable '{key}' for gallery '{galleryId}'");
}
return null;
@@ -78,16 +73,22 @@ namespace WeddingShare.Helpers
{
try
{
var value = this.GetEnvironmentVariable(key, galleryId);
if (!string.IsNullOrWhiteSpace(value))
var envValue = this.GetEnvironmentVariable(key, galleryId);
if (!string.IsNullOrWhiteSpace(envValue))
{
return value;
return envValue;
}
value = this.GetConfigValue(key);
if (!string.IsNullOrWhiteSpace(value))
envValue = this.GetEnvironmentVariable(key);
if (!string.IsNullOrWhiteSpace(envValue))
{
return value;
return envValue;
}
var configValue = this.GetConfigValue(key);
if (!string.IsNullOrWhiteSpace(configValue))
{
return configValue;
}
}
catch (Exception ex)

View File

@@ -5,15 +5,20 @@ namespace WeddingShare.Helpers.Database
{
public interface IDatabaseHelper
{
Task<int> GetGalleryCount();
Task<IEnumerable<string>> GetGalleryNames();
Task<List<GalleryModel>> GetAllGalleries();
Task<int?> GetGalleryId(string identifier);
Task<int?> GetGalleryIdByName(string name);
Task<string?> GetGalleryName(int id);
Task<GalleryModel?> GetGallery(int id);
Task<GalleryModel?> GetGallery(string name);
Task<GalleryModel?> AddGallery(GalleryModel model);
Task<GalleryModel?> EditGallery(GalleryModel model);
Task<bool> WipeGallery(GalleryModel model);
Task<bool> WipeAllGalleries();
Task<bool> DeleteGallery(GalleryModel model);
Task<List<GalleryItemModel>> GetAllGalleryItems(int? galleryId, GalleryItemState state = GalleryItemState.All, MediaType type = MediaType.All, GalleryOrder order = GalleryOrder.UploadedDesc, int limit = int.MaxValue, int page = 1);
Task<IDictionary<string, long>> GetGalleryItemCount(int? galleryId, GalleryItemState state = GalleryItemState.All, MediaType type = MediaType.All, ImageOrientation orientation = ImageOrientation.None);
Task<List<GalleryItemModel>> GetAllGalleryItems(int? galleryId, GalleryItemState state = GalleryItemState.All, MediaType type = MediaType.All, ImageOrientation orientation = ImageOrientation.None, GalleryGroup group = GalleryGroup.None, GalleryOrder order = GalleryOrder.Descending, int limit = int.MaxValue, int page = 1);
Task<int> GetPendingGalleryItemCount(int? galleryId = null);
Task<List<GalleryItemModel>> GetPendingGalleryItems(int? galleryId = null);
Task<GalleryItemModel?> GetPendingGalleryItem(int id);
@@ -22,7 +27,7 @@ namespace WeddingShare.Helpers.Database
Task<GalleryItemModel?> AddGalleryItem(GalleryItemModel model);
Task<GalleryItemModel?> EditGalleryItem(GalleryItemModel model);
Task<bool> DeleteGalleryItem(GalleryItemModel model);
Task<bool> InitAdminAccount(UserModel model);
Task<bool> InitOwnerAccount(UserModel model);
Task<bool> ValidateCredentials(string username, string password);
Task<List<UserModel>?> GetAllUsers();
Task<UserModel?> GetUser(int id);
@@ -38,5 +43,21 @@ namespace WeddingShare.Helpers.Database
Task<bool> ResetMultiFactorToDefault();
Task<bool> Import(string path);
Task<bool> Export(string path);
Task<IEnumerable<SettingModel>?> GetAllSettings(int? galleryId = null);
Task<SettingModel?> GetSetting(string id);
Task<SettingModel?> GetSetting(string id, int gallery);
Task<SettingModel?> GetGallerySpecificSetting(string id, int galleryId);
Task<SettingModel?> AddSetting(SettingModel model, int? galleryId = null);
Task<SettingModel?> EditSetting(SettingModel model, int? galleryId = null);
Task<SettingModel?> SetSetting(SettingModel model, int? galleryId = null);
Task<bool> DeleteSetting(SettingModel model, int? galleryId = null);
Task<bool> DeleteAllSettings(int? galleryId = null);
Task<CustomResourceModel?> GetCustomResource(int id);
Task<List<CustomResourceModel>> GetAllCustomResources();
Task<CustomResourceModel?> AddCustomResource(CustomResourceModel model);
Task<CustomResourceModel?> EditCustomResource(CustomResourceModel model);
Task<bool> DeleteCustomResource(CustomResourceModel model);
Task<IEnumerable<AuditLogModel>?> GetAuditLogs(string term = "", int limit = 100);
Task<AuditLogModel?> AddAuditLog(AuditLogModel model);
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,15 @@
using System.Reflection;
using System.Text.RegularExpressions;
using DbUp;
using DbUp.Engine;
using WeddingShare.Constants;
using WeddingShare.Enums;
using WeddingShare.Helpers.Database;
using WeddingShare.Models.Database;
namespace WeddingShare.Helpers.Dbup
{
public sealed class DbupMigrator(IEnvironmentWrapper environment, IConfiguration configuration, IDatabaseHelper database, IFileHelper fileHelper, ILoggerFactory loggerFactory) : BackgroundService
public sealed class DbupMigrator(IEnvironmentWrapper environment, IConfiguration configuration, IDatabaseHelper database, IFileHelper fileHelper, IEncryptionHelper encryption, ILoggerFactory loggerFactory) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
@@ -16,19 +18,21 @@ namespace WeddingShare.Helpers.Dbup
fileHelper.CreateDirectoryIfNotExists("config");
var config = new ConfigHelper(environment, configuration, loggerFactory.CreateLogger<ConfigHelper>());
var connString = config.GetOrDefault("Settings:Database:Connection_String", "Data Source=./config/wedding-share.db");
var connString = config.GetOrDefault(Settings.Database.ConnectionString, "Data Source=./config/wedding-share.db");
if (!string.IsNullOrWhiteSpace(connString))
{
DatabaseUpgradeResult? dbupResult;
var dbType = config.GetOrDefault("Settings:Database:Type", "sqlite")?.ToLower();
var dbType = config.GetOrDefault(Settings.Database.Type, "sqlite")?.ToLower();
switch (dbType)
{
case "sqlite":
dbupResult = new DbupSqliteHelper().Migrate(connString);
break;
case "mysql":
dbupResult = new DbupMySqlHelper().Migrate(connString);
var databaseName = config.GetOrDefault(Settings.Database.DatabaseName, "weddingshare");
dbupResult = new DbupMySqlHelper().Migrate(connString, databaseName);
break;
default:
var error = $"Database type '{dbType}' is not yet supported by this application";
@@ -41,15 +45,29 @@ namespace WeddingShare.Helpers.Dbup
logger.LogWarning($"DBUP failed with error: '{dbupResult?.Error?.Message}' - '{dbupResult?.Error?.ToString()}'");
}
var adminAccount = new UserModel() { Username = config.GetOrDefault("Settings:Account:Admin:Username", "admin"), Password = config.GetOrDefault("Settings:Account:Admin:Password", "admin") };
await database.InitAdminAccount(adminAccount);
if (config.GetOrDefault("Settings:Account:Admin:Log_Password", true))
{
logger.LogInformation($"Password: {adminAccount.Password}");
if (config.GetOrDefault(Settings.Database.SyncFromConfig, false))
{
logger.LogWarning($"Sync_From_Config set to true, wiping settings database and re-pulling values from config");
await database.DeleteAllSettings();
}
if (config.GetOrDefault("Security:2FA:Reset_To_Default", false))
var isDemoMode = config.GetOrDefault(Settings.IsDemoMode, false);
var username = !isDemoMode ? config.GetOrDefault(Settings.Account.Admin.Username, config.GetOrDefault(Settings.Account.Owner.Username, "admin")).ToLower() : "demo";
var ownerAccount = new UserModel()
{
Username = username,
Password = encryption.Encrypt(!isDemoMode ? config.GetOrDefault(Settings.Account.Admin.Password, config.GetOrDefault(Settings.Account.Owner.Password, "admin")) : "demo", username)
};
await database.InitOwnerAccount(ownerAccount);
await new DbupImporter(config, database, loggerFactory.CreateLogger<DbupImporter>()).ImportSettings();
if (config.GetOrDefault(Settings.Account.Owner.LogPassword, false))
{
logger.LogInformation($"Password: {ownerAccount.Password}");
}
if (config.GetOrDefault(Security.MultiFactor.ResetToDefault, false))
{
await database.ResetMultiFactorToDefault();
}
@@ -87,17 +105,19 @@ namespace WeddingShare.Helpers.Dbup
public class DbupMySqlHelper
{
public DatabaseUpgradeResult Migrate(string connectionString)
public DatabaseUpgradeResult Migrate(string connectionString, string database)
{
try
{
database = !string.IsNullOrWhiteSpace(database) ? database : Regex.Match(connectionString, "Database\\=(.+?)(;|$)", RegexOptions.IgnoreCase | RegexOptions.Multiline).Groups[1].Value;
var dbupBuilder = DeployChanges.To
.MySqlDatabase(connectionString)
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
.WithScriptNameComparer(new DbupScriptComparer())
.WithFilter(new DbupScriptFilter(DatabaseType.MySQL))
.LogToConsole();
dbupBuilder.Configure(c => c.Journal = new DbupMySqlTableJournal(() => c.ConnectionManager, () => c.Log, "weddingshare", "schemaversions"));
dbupBuilder.Configure(c => c.Journal = new DbupMySqlTableJournal(() => c.ConnectionManager, () => c.Log, database, "schemaversions"));
return dbupBuilder.Build().PerformUpgrade();
}

View File

@@ -0,0 +1,136 @@
using System.Collections;
using System.Reflection;
using WeddingShare.Helpers.Database;
using WeddingShare.Models.Database;
namespace WeddingShare.Helpers.Dbup
{
public class DbupImporter(IConfigHelper config, IDatabaseHelper database, ILogger<DbupImporter> logger)
{
public async Task ImportSettings()
{
try
{
var settings = await database.GetAllSettings();
if (settings == null || !settings.Any())
{
var systemKeys = GetAllKeys();
foreach (var key in systemKeys)
{
try
{
var configVal = config.Get(key);
if (!string.IsNullOrWhiteSpace(configVal))
{
await database.AddSetting(new SettingModel()
{
Id = key,
Value = configVal
});
}
}
catch { }
}
var galleries = (await database.GetAllGalleries())?.Where(x => !x.Identifier.Equals("All", StringComparison.OrdinalIgnoreCase));
if (galleries != null && galleries.Any())
{
var galleryKeys = GetKeys<Constants.Settings.Gallery>();
foreach (var gallery in galleries)
{
if (!string.IsNullOrWhiteSpace(gallery?.Name))
{
foreach (var key in galleryKeys)
{
try
{
var galleryOverride = config.GetEnvironmentVariable(key, gallery.Name);
if (!string.IsNullOrWhiteSpace(galleryOverride))
{
await database.AddSetting(new SettingModel()
{
Id = key,
Value = galleryOverride
}, gallery.Id);
}
}
catch { }
}
}
}
}
}
}
catch (Exception ex)
{
logger.LogError($"Failed to import settings at startup - {ex?.Message}", ex);
}
}
private IEnumerable<string> GetAllKeys()
{
var keys = new List<string>();
try
{
keys.AddRange(GetKeys<Constants.BackgroundServices>());
keys.AddRange(GetKeys<Constants.Notifications>());
keys.AddRange(GetKeys<Constants.Security>());
keys.AddRange(GetKeys<Constants.Settings>());
}
catch { }
return keys.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct();
}
private IEnumerable<string> GetKeys<T>(bool includeNesteted = true)
{
var keys = new List<string>();
try
{
var obj = Activator.CreateInstance<T>();
foreach (var val in GetConstants(typeof(T), includeNesteted))
{
keys.Add((string)(val.GetValue(obj) ?? string.Empty));
}
}
catch { }
return keys.Where(x => !string.IsNullOrWhiteSpace(x));
}
private FieldInfo[] GetConstants(Type type, bool includeNesteted)
{
var constants = new ArrayList();
try
{
if (includeNesteted)
{
var classInfos = type.GetNestedTypes(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
foreach (var ci in classInfos)
{
var consts = GetConstants(ci, includeNesteted);
if (consts != null && consts.Length > 0)
{
constants.AddRange(consts);
}
}
}
var fieldInfos = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
foreach (var fi in fieldInfos)
{
if (fi.IsLiteral && !fi.IsInitOnly)
{
constants.Add(fi);
}
}
}
catch { }
return (FieldInfo[])constants.ToArray(typeof(FieldInfo));
}
}
}

View File

@@ -1,12 +1,13 @@
using System.Security.Cryptography;
using System.Text;
using WeddingShare.Constants;
namespace WeddingShare.Helpers
{
public interface IEncryptionHelper
{
bool IsEncryptionEnabled();
string Encrypt(string value);
string Encrypt(string value, string? salt = null);
}
public class EncryptionHelper : IEncryptionHelper
@@ -16,13 +17,13 @@ namespace WeddingShare.Helpers
private readonly string _key;
private readonly string _salt;
public EncryptionHelper(IConfigHelper config)
public EncryptionHelper(ISettingsHelper settings)
{
_hashType = ParseHashType(config.GetOrDefault("Security:Encryption:HashType", "SHA256"));
_iterations = config.GetOrDefault("Security:Encryption:Iterations", 1000);
_hashType = ParseHashType(settings.GetOrDefault(Security.Encryption.HashType, "SHA256").Result);
_iterations = settings.GetOrDefault(Security.Encryption.Iterations, 1000).Result;
_key = config.GetOrDefault("Security:Encryption:Key", string.Empty);
_salt = config.GetOrDefault("Security:Encryption:Salt", "WUtlVOvC2a6ol9M6ZidO5sJkQxYMolyasFid2Fyqvjd0uucAjYy5EsHPxdeplFRj");
_key = settings.GetOrDefault(Security.Encryption.Key, string.Empty).Result;
_salt = settings.GetOrDefault(Security.Encryption.Salt, "WUtlVOvC2a6ol9M6ZidO5sJkQxYMolyasFid2Fyqvjd0uucAjYy5EsHPxdeplFRj").Result;
}
public bool IsEncryptionEnabled()
@@ -30,13 +31,13 @@ namespace WeddingShare.Helpers
return !string.IsNullOrWhiteSpace(_key) && !string.IsNullOrWhiteSpace(_salt);
}
public string Encrypt(string value)
public string Encrypt(string value, string? salt = null)
{
var enabled = this.IsEncryptionEnabled();
if (enabled)
{
var clearBytes = Encoding.Unicode.GetBytes(value);
var saltBytes = Encoding.Unicode.GetBytes(_salt);
var saltBytes = Encoding.Unicode.GetBytes(salt ?? _salt);
using (var encryptor = Aes.Create())
{

View File

@@ -6,5 +6,7 @@
public const int Unauthorized = 401;
public const int InvalidSecretKey = 402;
public const int GalleryCreationNotAllowed = 403;
public const int GalleryLimitReached = 405;
public const int InvalidGalleryId = 406;
}
}

View File

@@ -1,5 +1,7 @@
using System.Security.Cryptography;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace WeddingShare.Helpers
{
@@ -8,15 +10,20 @@ namespace WeddingShare.Helpers
bool DirectoryExists(string path);
bool CreateDirectoryIfNotExists(string path);
bool DeleteDirectoryIfExists(string path, bool recursive = true);
bool PurgeDirectory(string path);
string[] GetDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.AllDirectories);
string[] GetFiles(string path, string pattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories);
bool FileExists(string path);
long FileSize(string path);
bool DeleteFileIfExists(string path);
bool MoveFileIfExists(string source, string destination);
long GetDirectorySize(string path);
Task<byte[]> ReadAllBytes(string path);
Task SaveFile(IFormFile file, string path, FileMode mode);
Task<string> GetChecksum(string path);
Task<DateTime?> GetCreationDatetime(string path);
string BytesToHumanReadable(long bytes, int decimalPlaces = 0);
string SanitizeFilename(string filename);
}
public class FileHelper : IFileHelper
@@ -57,6 +64,12 @@ namespace WeddingShare.Helpers
return false;
}
public bool PurgeDirectory(string path)
{
DeleteDirectoryIfExists(path);
return CreateDirectoryIfNotExists(path);
}
public string[] GetDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.AllDirectories)
{
return Directory.GetDirectories(path, pattern, searchOption);
@@ -72,6 +85,11 @@ namespace WeddingShare.Helpers
return File.Exists(path);
}
public long FileSize(string path)
{
return new FileInfo(path).Length;
}
public bool DeleteFileIfExists(string path)
{
if (FileExists(path))
@@ -153,5 +171,55 @@ namespace WeddingShare.Helpers
return checksum;
});
}
public async Task<DateTime?> GetCreationDatetime(string path)
{
return await Task.Run(() =>
{
try
{
return new FileInfo(path).CreationTimeUtc;
}
catch
{
return DateTime.UtcNow;
}
});
}
public string BytesToHumanReadable(long bytes, int decimalPlaces = 0)
{
var sizes = new string[] { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
var place = 0;
var total = 0.0;
var decimalFormat = "###0.";
for (var i = 0; i < decimalPlaces; i++)
{
decimalFormat += "0";
}
if (bytes >= 0)
{
try
{
long b = Math.Abs(bytes);
place = Convert.ToInt32(Math.Floor(Math.Log(b ,1000)));
double num = Math.Round(b / Math.Pow(1000, place), 2);
total = Math.Sign(bytes) * num;
}
catch { }
}
return total.ToString($"{decimalFormat.TrimEnd('.')} {sizes[place]}");
}
public string SanitizeFilename(string filename)
{
var invalidChars = Regex.Escape(new string(Path.GetInvalidFileNameChars()));
var regex = string.Format(@"([{0}]*\.+$)|([{0}]+)", invalidChars);
return Regex.Replace(filename, regex, string.Empty, RegexOptions.Compiled);
}
}
}

View File

@@ -1,163 +1,10 @@
using WeddingShare.Helpers.Database;
namespace WeddingShare.Helpers
namespace WeddingShare.Helpers
{
public interface IGalleryHelper
public class GalleryHelper
{
string GetConfig(string? galleryId, string key, string defaultValue);
int GetConfig(string? galleryId, string key, int defaultValue);
long GetConfig(string? galleryId, string key, long defaultValue);
decimal GetConfig(string? galleryId, string key, decimal defaultValue);
double GetConfig(string? galleryId, string key, double defaultValue);
bool GetConfig(string? galleryId, string key, bool defaultValue);
DateTime? GetConfig(string section, string key, DateTime? defaultValue);
Task<string?> GetSecretKey(string galleryId);
}
public class GalleryHelper : IGalleryHelper
{
private readonly IConfigHelper _config;
private readonly IDatabaseHelper _database;
public GalleryHelper(IConfigHelper config, IDatabaseHelper database)
public static string GenerateGalleryIdentifier()
{
_config = config;
_database = database;
}
public string GetConfig(string? galleryId, string key, string defaultValue = "")
{
string? value = null;
try
{
value = _config.Get($"Settings:{key}", galleryId ?? "default");
if (string.IsNullOrWhiteSpace(value))
{
value = _config.Get($"Settings:{key}");
if (string.IsNullOrWhiteSpace(value))
{
value = defaultValue;
}
}
}
catch
{
}
return !string.IsNullOrWhiteSpace(value) ? value : defaultValue;
}
public int GetConfig(string? galleryId, string key, int defaultValue)
{
try
{
var value = this.GetConfig(galleryId, key, string.Empty);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToInt32(value);
}
}
catch { }
return defaultValue;
}
public long GetConfig(string? galleryId, string key, long defaultValue)
{
try
{
var value = this.GetConfig(galleryId, key, string.Empty);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToInt64(value);
}
}
catch { }
return defaultValue;
}
public decimal GetConfig(string? galleryId, string key, decimal defaultValue)
{
try
{
var value = this.GetConfig(galleryId, key, string.Empty);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToDecimal(value);
}
}
catch { }
return defaultValue;
}
public double GetConfig(string? galleryId, string key, double defaultValue)
{
try
{
var value = this.GetConfig(galleryId, key, string.Empty);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToDouble(value);
}
}
catch { }
return defaultValue;
}
public bool GetConfig(string? galleryId, string key, bool defaultValue)
{
try
{
var value = this.GetConfig(galleryId, key, string.Empty);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToBoolean(value);
}
}
catch { }
return defaultValue;
}
public DateTime? GetConfig(string? galleryId, string key, DateTime? defaultValue)
{
try
{
var value = this.GetConfig(galleryId, key, string.Empty);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToDateTime(value);
}
}
catch { }
return defaultValue;
}
public async Task<string?> GetSecretKey(string galleryId)
{
try
{
var secretKey = _config.Get($"Settings:Gallery:Secret_Key", galleryId ?? "default");
if (string.IsNullOrWhiteSpace(secretKey))
{
secretKey = (await _database.GetGallery(galleryId ?? "default"))?.SecretKey;
if (string.IsNullOrWhiteSpace(secretKey))
{
secretKey = _config.Get("Settings:Gallery:Secret_Key");
}
}
return secretKey;
}
catch
{
return null;
}
return Guid.NewGuid().ToString().Replace("-", string.Empty).ToLower();
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Text.RegularExpressions;
using System.Web;
namespace WeddingShare.Helpers
{
public class HtmlSanitizer
{
public static string Sanitize(string input)
{
var output = input;
if (!string.IsNullOrWhiteSpace(output))
{
output = SanitizeHtmlTags(input, new[] { ".*" });
output = SanitizeHtmlAttributes(output, new[] { ".*" });
output = SanitizeLinks(output);
output = HttpUtility.HtmlEncode(output);
}
return output;
}
public static string SanitizeHtmlTags(string input, string[] tags)
{
var output = input;
foreach (var tag in tags)
{
output = new Regex($"<\\/?\\s*{tag}\\s*[^>]*>", RegexOptions.IgnoreCase).Replace(output, string.Empty);
}
return output;
}
public static string SanitizeHtmlAttributes(string input, string[] attrs)
{
var output = input;
foreach (var attr in attrs)
{
output = new Regex($"{attr}\\s*=\\s*['\"].*?['\"]", RegexOptions.IgnoreCase).Replace(output, string.Empty);
}
return output;
}
public static string SanitizeLinks(string input)
{
var output = input;
output = new Regex(@"href\s*=\s*['""]javascript:[^'""]*['""]", RegexOptions.IgnoreCase).Replace(output, string.Empty);
output = new Regex(@"(http|https):\/\/[^\s<>]+", RegexOptions.IgnoreCase).Replace(output, string.Empty);
return output;
}
public static bool MayContainXss(string input)
{
var sanitized = Sanitize(input);
return !string.Equals(input, sanitized);
}
}
}

View File

@@ -1,6 +1,5 @@
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using WeddingShare.Enums;
@@ -12,6 +11,7 @@ namespace WeddingShare.Helpers
public interface IImageHelper
{
Task<bool> GenerateThumbnail(string filePath, string savePath, int size = 720);
Task<ImageOrientation> GetOrientation(string path);
ImageOrientation GetOrientation(Image img);
MediaType GetMediaType(string filePath);
Task<bool> DownloadFFMPEG(string path);
@@ -123,6 +123,28 @@ namespace WeddingShare.Helpers
return MediaType.Unknown;
}
public async Task<ImageOrientation> GetOrientation(string path)
{
var orientation = ImageOrientation.None;
if (_fileHelper.FileExists(path))
{
try
{
using (var img = await Image.LoadAsync(path))
{
orientation = this.GetOrientation(img);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to get image orientation- '{path}'");
}
}
return orientation;
}
public ImageOrientation GetOrientation(Image img)
{
if (img != null)

View File

@@ -0,0 +1,103 @@
using System.Globalization;
namespace WeddingShare.Helpers
{
public interface ILanguageHelper
{
public List<CultureInfo> DetectSupportedCultures();
public Task<List<CultureInfo>> DetectSupportedCulturesAsync();
public Task<bool> IsCultureSupported(string culture);
public Task<string> GetOrFallbackCulture(string culture, string fallback);
}
public class LanguageHelper : ILanguageHelper
{
public List<CultureInfo> DetectSupportedCultures()
{
var supportedCultures = new List<CultureInfo>();
try
{
var resourceFiles = Directory.GetFiles(Path.Combine("Resources", "Lang"), "*.resx");
var detectedCultures = resourceFiles
.Select(x => Path.GetFileNameWithoutExtension(x))
.Where(x => x.Contains(".") && x.Contains("-"))
.Select(x => x.Split('.').LastOrDefault());
foreach (var detectedCulture in detectedCultures)
{
if (!string.IsNullOrWhiteSpace(detectedCulture))
{
try
{
supportedCultures.Add(new CultureInfo(detectedCulture));
}
catch { }
}
}
}
catch
{
supportedCultures.Add(new CultureInfo("en-GB"));
}
return supportedCultures;
}
public Task<List<CultureInfo>> DetectSupportedCulturesAsync()
{
return Task.Run(DetectSupportedCultures);
}
public async Task<bool> IsCultureSupported(string culture)
{
return this.IsCultureSupported(culture, await DetectSupportedCulturesAsync());
}
public bool IsCultureSupported(string culture, List<CultureInfo> supported)
{
try
{
if (!string.IsNullOrWhiteSpace(culture) && supported.Any(x => string.Equals(x.Name, culture, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
catch { }
return false;
}
public async Task<string> GetOrFallbackCulture(string culture, string fallback)
{
return this.GetOrFallbackCulture(culture, fallback, await DetectSupportedCulturesAsync());
}
public string GetOrFallbackCulture(string culture, string fallback, List<CultureInfo> supported)
{
try
{
if (!string.IsNullOrWhiteSpace(culture))
{
var match = supported.FirstOrDefault(x => string.Equals(x.Name, culture, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
return match.Name;
}
}
if (!string.IsNullOrWhiteSpace(fallback))
{
var match = supported.FirstOrDefault(x => string.Equals(x.Name, fallback, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
return match.Name;
}
}
}
catch { }
return "en-GB";
}
}
}

View File

@@ -1,178 +0,0 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;
using WeddingShare.Models.Migrator;
namespace WeddingShare.Helpers.Migrators
{
public class KeyHelper
{
public static List<KeyMigrator> GetAlternateVersions(string key, string? galleryId = null)
{
var keys = new List<KeyMigrator>();
try
{
key = key.Trim();
keys.Add(new KeyMigrator(1, key));
if (string.Equals(key, "Settings:Home_Link", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_Home_Link", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Disable_Guest_Gallery_Creation", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Guest_Gallery_Creation", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Themes:Enabled", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_Dark_Mode", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Themes:Enabled", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_Themes", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Themes:Default", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Default_Theme"));
}
else if (string.Equals(key, "Settings:Identity_Check:Enabled", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Show_Identity_Request"));
}
else if (string.Equals(key, "Settings:Show_Identity_Request", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Identity_Check:Enabled", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Account:Admins:Username", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Admin:Username"));
}
else if (string.Equals(key, "Settings:Account:Admins:Password", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Admin:Password"));
}
else if (string.Equals(key, "Settings:Account:Admins:Log_Password", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Admin:Log_Password"));
}
else if (string.Equals(key, "Settings:Gallery:QR_Code", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_QR_Code", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Gallery:Secret_Key", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Secret_Key"));
}
else if (string.Equals(key, "Settings:Gallery:Columns", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Gallery_Columns"));
}
else if (string.Equals(key, "Settings:Gallery:Items_Per_Page", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Gallery_Items_Per_Page"));
}
else if (string.Equals(key, "Settings:Gallery:Quote", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Gallery_Quote"));
}
else if (string.Equals(key, "Settings:Gallery:Retain_Rejected_Items", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Retain_Rejected_Items"));
}
else if (string.Equals(key, "Settings:Gallery:Full_Width", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Full_Width_Gallery"));
}
else if (string.Equals(key, "Settings:Gallery:Upload", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_Upload", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Gallery:Download", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_Download", (v) => { return (bool.Parse(v) == false).ToString(); }));
}
else if (string.Equals(key, "Settings:Gallery:Require_Review", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Require_Review"));
}
else if (string.Equals(key, "Settings:Gallery:Review_Counter", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Disable_Review_Counter"));
}
else if (string.Equals(key, "Settings:Gallery:Prevent_Duplicates", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Prevent_Duplicates"));
}
else if (string.Equals(key, "Settings:Gallery:Idle_Refresh_Mins", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Idle_Gallery_Refresh_Mins"));
}
else if (string.Equals(key, "Settings:Gallery:Max_Size_Mb", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Max_Size_Mb"));
}
else if (string.Equals(key, "Settings:Gallery:Max_File_Size_Mb", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Max_File_Size_Mb"));
}
else if (string.Equals(key, "Settings:Gallery:Allowed_File_Types", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Allowed_File_Types"));
}
else if (string.Equals(key, "Settings:Gallery:Default_View", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Settings:Default_Gallery_View"));
}
else if (string.Equals(key, "BackgroundServices:Schedules:Directory_Scanner", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "BackgroundServices:Directory_Scanner_Interval"));
}
else if (string.Equals(key, "BackgroundServices:Schedules:Email_Report", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "BackgroundServices:Email_Report_Interval"));
}
else if (string.Equals(key, "BackgroundServices:Schedules:Cleanup", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "BackgroundServices:Cleanup_Interval"));
}
else if (string.Equals(key, "Security:Headers:Enabled", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Security:Set_Headers"));
}
else if (string.Equals(key, "Security:Headers:X_Frame_Options", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Security:X_Frame_Options"));
}
else if (string.Equals(key, "Security:Headers:X_Content_Type_Options", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Security:X_Content_Type_Options"));
}
else if (string.Equals(key, "Security:Headers:CSP", StringComparison.OrdinalIgnoreCase))
{
keys.Add(new KeyMigrator(2, "Security:CSP_Header"));
}
if (!string.IsNullOrWhiteSpace(galleryId) && keys.Any())
{
var priority = keys.Max(k => k.Priority) * -1;
var count = keys.Count;
for (var i = 0; i < count; i++)
{
var k = keys[i];
keys.Add(new KeyMigrator(priority + k.Priority, $"{k.Key}_{galleryId}", k.MigrationAction));
}
}
}
catch { }
keys = keys.Distinct().OrderBy(x => x.Priority).ToList();
return keys;
}
}
}

View File

@@ -1,59 +1,58 @@
using System.Net;
using System.Net.Mail;
using System.Text;
namespace WeddingShare.Helpers.Notifications
{
public class EmailHelper : INotificationHelper
{
private readonly IConfigHelper _config;
private readonly ISettingsHelper _settings;
private readonly ISmtpClientWrapper _client;
private readonly ILogger _logger;
public EmailHelper(IConfigHelper config, ISmtpClientWrapper client, ILogger<EmailHelper> logger)
public EmailHelper(ISettingsHelper settings, ISmtpClientWrapper client, ILogger<EmailHelper> logger)
{
_config = config;
_settings = settings;
_client = client;
_logger = logger;
}
public async Task<bool> Send(string title, string message, string? actionLink = null)
{
if (_config.GetOrDefault("Notifications:Smtp:Enabled", false))
if (await _settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, false))
{
try
{
var recipients = _config.GetOrDefault("Notifications:Smtp:Recipient", string.Empty)?.Split(new char[] { ';', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)?.Select(x => new MailAddress(x));
var recipients = (await _settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, string.Empty))?.Split(new char[] { ';', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)?.Select(x => new MailAddress(x));
if (recipients != null && recipients.Any())
{
var host = _config.GetOrDefault("Notifications:Smtp:Host", string.Empty);
var host = await _settings.GetOrDefault(Constants.Notifications.Smtp.Host, string.Empty);
if (!string.IsNullOrWhiteSpace(host))
{
var port = _config.GetOrDefault("Notifications:Smtp:Port", 587);
var port = await _settings.GetOrDefault(Constants.Notifications.Smtp.Port, 587);
if (port > 0)
{
var from = _config.GetOrDefault("Notifications:Smtp:From", string.Empty);
var from = await _settings.GetOrDefault(Constants.Notifications.Smtp.From, string.Empty);
if (!string.IsNullOrWhiteSpace(from))
{
var sentToAll = true;
using (var smtp = new SmtpClient(host, port))
{
var username = _config.GetOrDefault("Notifications:Smtp:Username", string.Empty);
var password = _config.GetOrDefault("Notifications:Smtp:Password", string.Empty);
var username = await _settings.GetOrDefault(Constants.Notifications.Smtp.Username, string.Empty);
var password = await _settings.GetOrDefault(Constants.Notifications.Smtp.Password, string.Empty);
if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
{
smtp.UseDefaultCredentials = false;
smtp.Credentials = new NetworkCredential(username, password);
}
smtp.EnableSsl = _config.GetOrDefault("Notifications:Smtp:Use_SSL", false);
smtp.EnableSsl = await _settings.GetOrDefault(Constants.Notifications.Smtp.UseSSL, false);
var sender = new MailAddress(from, _config.GetOrDefault("Notifications:Smtp:DisplayName", "WeddingShare"));
var sender = new MailAddress(from, await _settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, "WeddingShare"));
foreach (var to in recipients)
{
try
{
await _client.SendMailAsync(smtp, new MailMessage(new MailAddress(from, _config.GetOrDefault("Notifications:Smtp:DisplayName", "WeddingShare")), to)
await _client.SendMailAsync(smtp, new MailMessage(new MailAddress(from, await _settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, "WeddingShare")), to)
{
Sender = sender,
Subject = title,

View File

@@ -2,39 +2,39 @@
{
public class GotifyHelper : INotificationHelper
{
private readonly IConfigHelper _config;
private readonly ISettingsHelper _settings;
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _logger;
public GotifyHelper(IConfigHelper config, IHttpClientFactory clientFactory, ILogger<GotifyHelper> logger)
public GotifyHelper(ISettingsHelper settings, IHttpClientFactory clientFactory, ILogger<GotifyHelper> logger)
{
_config = config;
_settings = settings;
_clientFactory = clientFactory;
_logger = logger;
}
public async Task<bool> Send(string title, string message, string? actionLink = null)
{
if (string.IsNullOrWhiteSpace(_config.GetOrDefault("Notifications:Gotify:Endpoint", string.Empty)))
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, string.Empty)))
{
_logger.LogWarning($"Invalid Gotify endpoint specified");
return false;
}
if (string.IsNullOrWhiteSpace(_config.GetOrDefault("Notifications:Gotify:Token", string.Empty)))
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Gotify.Token, string.Empty)))
{
_logger.LogWarning($"Invalid Gotify token specified");
return false;
}
if (_config.GetOrDefault("Notifications:Gotify:Enabled", false))
if (await _settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, false))
{
try
{
var token = _config.GetOrDefault("Notifications:Gotify:Token", string.Empty);
var token = await _settings.GetOrDefault(Constants.Notifications.Gotify.Token, string.Empty);
if (!string.IsNullOrWhiteSpace(token))
{
var priority = _config.GetOrDefault("Notifications:Gotify:Priority", 4);
var priority = await _settings.GetOrDefault(Constants.Notifications.Gotify.Priority, 4);
if (priority > 0)
{
message = !string.IsNullOrWhiteSpace(actionLink) ? $"{message} - Visit - {actionLink}" : message;

View File

@@ -2,14 +2,14 @@
{
public class NotificationBroker : INotificationHelper
{
private readonly IConfigHelper _config;
private readonly ISettingsHelper _settings;
private readonly ISmtpClientWrapper _smtp;
private readonly IHttpClientFactory _clientFactory;
private readonly ILoggerFactory _logger;
public NotificationBroker(IConfigHelper config, ISmtpClientWrapper smtp, IHttpClientFactory clientFactory, ILoggerFactory logger)
public NotificationBroker(ISettingsHelper settings, ISmtpClientWrapper smtp, IHttpClientFactory clientFactory, ILoggerFactory logger)
{
_config = config;
_settings = settings;
_smtp = smtp;
_clientFactory = clientFactory;
_logger = logger;
@@ -21,19 +21,19 @@
var ntfySent = true;
var gotifySent = true;
if (_config.GetOrDefault("Notifications:Smtp:Enabled", false))
if (await _settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, false))
{
emailSent = await new EmailHelper(_config, _smtp, _logger.CreateLogger<EmailHelper>()).Send(title, message, actionLink);
emailSent = await new EmailHelper(_settings, _smtp, _logger.CreateLogger<EmailHelper>()).Send(title, message, actionLink);
}
if (_config.GetOrDefault("Notifications:Ntfy:Enabled", false))
if (await _settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, false))
{
ntfySent = await new NtfyHelper(_config, _clientFactory, _logger.CreateLogger<NtfyHelper>()).Send(title, message, actionLink);
ntfySent = await new NtfyHelper(_settings, _clientFactory, _logger.CreateLogger<NtfyHelper>()).Send(title, message, actionLink);
}
if (_config.GetOrDefault("Notifications:Gotify:Enabled", false))
if (await _settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, false))
{
gotifySent = await new GotifyHelper(_config, _clientFactory, _logger.CreateLogger<GotifyHelper>()).Send(title, message, actionLink);
gotifySent = await new GotifyHelper(_settings, _clientFactory, _logger.CreateLogger<GotifyHelper>()).Send(title, message, actionLink);
}
return emailSent && ntfySent && gotifySent;

View File

@@ -1,44 +1,45 @@
namespace WeddingShare.Helpers.Notifications
using WeddingShare.Constants;
namespace WeddingShare.Helpers.Notifications
{
public class NtfyHelper : INotificationHelper
{
private readonly IConfigHelper _config;
private readonly ISettingsHelper _settings;
private readonly IHttpClientFactory _clientFactory;
private readonly ILogger _logger;
public NtfyHelper(IConfigHelper config, IHttpClientFactory clientFactory, ILogger<NtfyHelper> logger)
public NtfyHelper(ISettingsHelper settings, IHttpClientFactory clientFactory, ILogger<NtfyHelper> logger)
{
_config = config;
_settings = settings;
_clientFactory = clientFactory;
_logger = logger;
}
public async Task<bool> Send(string title, string message, string? actionLink = null)
{
if (string.IsNullOrWhiteSpace(_config.GetOrDefault("Notifications:Ntfy:Endpoint", string.Empty)))
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, string.Empty)))
{
_logger.LogWarning($"Invalid Ntfy endpoint specified");
return false;
}
if (string.IsNullOrWhiteSpace(_config.GetOrDefault("Notifications:Ntfy:Token", string.Empty)))
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Ntfy.Token, string.Empty)))
{
_logger.LogWarning($"Invalid Ntfy token specified");
return false;
_logger.LogWarning($"No Ntfy token specified. It is recommended that you secure your topic with a token");
}
if (_config.GetOrDefault("Notifications:Ntfy:Enabled", false))
if (await _settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, false))
{
try
{
var topic = _config.GetOrDefault("Notifications:Ntfy:Topic", "WeddingShare");
var topic = await _settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, "WeddingShare");
if (!string.IsNullOrWhiteSpace(topic))
{
var priority = _config.GetOrDefault("Notifications:Ntfy:Priority", 4);
var priority = await _settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, 4);
if (priority > 0)
{
var defaultIcon = "https://github.com/Cirx08/WeddingShare/blob/main/WeddingShare/wwwroot/images/logo.png?raw=true";
var icon = _config.GetOrDefault("Settings:Logo", defaultIcon);
var icon = await _settings.GetOrDefault(Settings.Basic.Logo, defaultIcon);
icon = !icon.StartsWith('.') && !icon.StartsWith('/') ? icon : defaultIcon;
var client = _clientFactory.CreateClient("NtfyClient");

View File

@@ -0,0 +1,235 @@
using System.Text;
using WeddingShare.Constants;
using WeddingShare.Helpers.Database;
using WeddingShare.Models.Database;
namespace WeddingShare.Helpers
{
public interface ISettingsHelper
{
Task<SettingModel?> Get(string key, int? galleryId = null);
Task<string> GetOrDefault(string key, string defaultValue, int? galleryId = null);
Task<int> GetOrDefault(string key, int defaultValue, int? galleryId = null);
Task<long> GetOrDefault(string key, long defaultValue, int? galleryId = null);
Task<decimal> GetOrDefault(string key, decimal defaultValue, int? galleryId = null);
Task<double> GetOrDefault(string key, double defaultValue, int? galleryId = null);
Task<bool> GetOrDefault(string key, bool defaultValue, int? galleryId = null);
Task<DateTime?> GetOrDefault(string key, DateTime? defaultValue, int? galleryId = null);
Task<SettingModel?> SetSetting(string key, string value, int? galleryId = null);
Task<bool> DeleteSetting(string key, int? galleryId = null);
Task<string> GetReleaseVersion(int places = 3);
}
public class SettingsHelper : ISettingsHelper
{
private readonly IDatabaseHelper _databaseHelper;
private readonly IConfigHelper _configHelper;
private readonly ILogger _logger;
public SettingsHelper(IDatabaseHelper databaseHelper, IConfigHelper configHelper, ILogger<SettingsHelper> logger)
{
_databaseHelper = databaseHelper;
_configHelper = configHelper;
_logger = logger;
}
public async Task<SettingModel?> Get(string key, int? galleryId = null)
{
if (!string.IsNullOrWhiteSpace(key))
{
try
{
var dbValue = galleryId != null ? await _databaseHelper.GetSetting(key, galleryId.Value) : await _databaseHelper.GetSetting(key);
if (dbValue != null)
{
return dbValue;
}
var configValue = _configHelper.Get(key);
if (configValue != null)
{
return new SettingModel()
{
Id = key.ToUpper(),
Value = configValue
};
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, $"Failed to find key '{key}' in either database or config");
}
}
return null;
}
public async Task<string> GetOrDefault(string key, string defaultValue, int? galleryId = null)
{
try
{
var value = (await this.Get(key, galleryId))?.Value;
if (!string.IsNullOrWhiteSpace(value))
{
return value;
}
}
catch { }
return defaultValue;
}
public async Task<int> GetOrDefault(string key, int defaultValue, int? galleryId = null)
{
try
{
var value = await this.GetOrDefault(key, string.Empty, galleryId);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToInt32(value);
}
}
catch { }
return defaultValue;
}
public async Task<long> GetOrDefault(string key, long defaultValue, int? galleryId = null)
{
try
{
var value = await this.GetOrDefault(key, string.Empty, galleryId);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToInt64(value);
}
}
catch { }
return defaultValue;
}
public async Task<decimal> GetOrDefault(string key, decimal defaultValue, int? galleryId = null)
{
try
{
var value = await this.GetOrDefault(key, string.Empty, galleryId);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToDecimal(value);
}
}
catch { }
return defaultValue;
}
public async Task<double> GetOrDefault(string key, double defaultValue, int? galleryId = null)
{
try
{
var value = await this.GetOrDefault(key, string.Empty, galleryId);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToDouble(value);
}
}
catch { }
return defaultValue;
}
public async Task<bool> GetOrDefault(string key, bool defaultValue, int? galleryId = null)
{
try
{
var value = await this.GetOrDefault(key, string.Empty, galleryId);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToBoolean(value);
}
}
catch { }
return defaultValue;
}
public async Task<DateTime?> GetOrDefault(string key, DateTime? defaultValue, int? galleryId = null)
{
try
{
var value = await this.GetOrDefault(key, string.Empty, galleryId);
if (!string.IsNullOrWhiteSpace(value))
{
return Convert.ToDateTime(value);
}
}
catch { }
return defaultValue;
}
public async Task<SettingModel?> SetSetting(string key, string value, int? galleryId = null)
{
if (!string.IsNullOrWhiteSpace(key))
{
return await _databaseHelper.SetSetting(new SettingModel()
{
Id = key,
Value = value
}, galleryId);
}
return null;
}
public async Task<bool> DeleteSetting(string key, int? galleryId = null)
{
if (!string.IsNullOrWhiteSpace(key))
{
return await _databaseHelper.DeleteSetting(new SettingModel()
{
Id = key.ToUpper()
}, galleryId);
}
return false;
}
public async Task<bool> DeleteAllSettings(int? galleryId = null)
{
return await _databaseHelper.DeleteAllSettings(galleryId);
}
public async Task<string> GetReleaseVersion(int places = 3)
{
try
{
var versionNumberParts = (await this.GetOrDefault(Release.Version, "1.0.0"))?.Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (versionNumberParts != null && versionNumberParts.Length > 0)
{
var builder = new StringBuilder();
for (var i = 0; i < places; i++)
{
if (i < versionNumberParts.Length)
{
builder.Append($".{versionNumberParts[i]}");
}
else
{
builder.Append(".0");
}
}
return builder.ToString().Trim('.');
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Failed to build release version string - {ex?.Message}");
}
return "1.0.0";
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using WeddingShare.Constants;
namespace WeddingShare.Helpers
{
@@ -15,11 +16,11 @@ namespace WeddingShare.Helpers
public class UrlHelper : IUrlHelper
{
private readonly IConfigHelper _config;
private readonly ISettingsHelper _settings;
public UrlHelper(IConfigHelper config)
public UrlHelper(ISettingsHelper settings)
{
_config = config;
_settings = settings;
}
public string GenerateFullUrl(HttpRequest? ctx, string? path, List<KeyValuePair<string, string>>? append = null, List<string>? exclude = null)
@@ -31,8 +32,8 @@ namespace WeddingShare.Helpers
{
if (ctx != null)
{
var scheme = _config.GetOrDefault("Settings:Force_Https", false) ? "https" : ctx.Scheme;
var host = ExtractHost(_config.GetOrDefault("Settings:Base_Url", ctx.Host.Value));
var scheme = _settings.GetOrDefault(Settings.Basic.ForceHttps, false).Result ? "https" : ctx.Scheme;
var host = ExtractHost(_settings.GetOrDefault(Settings.Basic.BaseUrl, ctx.Host.Value).Result);
return $"{scheme}://{host}/{path?.TrimStart('/')}";
}

View File

@@ -0,0 +1,10 @@
namespace WeddingShare.Models.Database
{
public class AuditLogModel
{
public int Id { get; set; }
public string Message { get; set; }
public string Username { get; set; }
public DateTime Timestamp { get; set; }
}
}

View File

@@ -0,0 +1,10 @@
namespace WeddingShare.Models.Database
{
public class CustomResourceModel
{
public int Id { get; set; }
public string? FileName { get; set; }
public string? UploadedBy { get; set; }
public int Owner { get; set; }
}
}

View File

@@ -5,29 +5,33 @@ namespace WeddingShare.Models.Database
public class GalleryItemModel
{
public GalleryItemModel()
: this(0, 0, string.Empty, string.Empty, null, null, MediaType.Unknown, GalleryItemState.Pending)
: this(0, 0, string.Empty, null, null, null, MediaType.Unknown, ImageOrientation.None, GalleryItemState.Pending, 0)
{
}
public GalleryItemModel(int id, int galleryId, string galleryName, string title, string? uploadedBy, string? checksum, MediaType mediaType, GalleryItemState state)
public GalleryItemModel(int id, int galleryId, string title, string? uploadedBy, DateTime? uploadedDate, string? checksum, MediaType mediaType, ImageOrientation orientation, GalleryItemState state, long file_size)
{
Id = id;
GalleryId = galleryId;
GalleryName = galleryName;
Title = title;
UploadedBy = uploadedBy;
UploadedDate = uploadedDate;
Checksum = checksum;
MediaType = mediaType;
Orientation = orientation;
State = state;
FileSize = file_size;
}
public int Id { get; set; }
public int GalleryId { get; set; }
public string GalleryName { get; set; }
public string Title { get; set; }
public string? UploadedBy { get; set; }
public DateTime? UploadedDate { get; set; }
public string? Checksum { get; set; }
public MediaType MediaType { get; set; }
public ImageOrientation Orientation { get; set; }
public GalleryItemState State { get; set; }
public long FileSize { get; set; }
}
}

View File

@@ -1,12 +1,22 @@
namespace WeddingShare.Models.Database
using WeddingShare.Helpers;
namespace WeddingShare.Models.Database
{
public class GalleryModel
{
public int Id { get; set; }
public string Name { get; set; }
public string Identifier { get; set; } = GalleryHelper.GenerateGalleryIdentifier();
public string Name { get; set; } = "Unknown";
public string? SecretKey { get; set; }
public int TotalItems { get; set; }
public int ApprovedItems { get; set; }
public int PendingItems { get; set; }
public long TotalGallerySize { get; set; }
public int Owner { get; set; }
public string CalculateUsage(long maxSizeMB = long.MaxValue)
{
return ((double)(TotalGallerySize / (double)(maxSizeMB * 1000000L))).ToString("0.00%");
}
}
}

View File

@@ -0,0 +1,22 @@
namespace WeddingShare.Models.Database
{
public class SettingModel
{
public string Id { get; set; }
public string? Value { get; set; }
public T Parse<T>(T defaultValue)
{
if (!string.IsNullOrWhiteSpace(this.Value))
{
try
{
return (T)Convert.ChangeType(this.Value, typeof(T));
}
catch { }
}
return defaultValue;
}
}
}

View File

@@ -1,4 +1,6 @@
namespace WeddingShare.Models.Database
using WeddingShare.Enums;
namespace WeddingShare.Models.Database
{
public class UserModel
{
@@ -6,9 +8,12 @@
public string Username { get; set; }
public string? Email { get; set; }
public string? Password { get; set; }
public string? CPassword { get; set; }
public int FailedLogins { get; set; }
public DateTime? LockoutUntil { get; set; }
public string? MultiFactorToken { get; set; }
public AccountState State { get; set; } = AccountState.Active;
public UserLevel Level { get; set; } = UserLevel.Basic;
public bool IsLockedOut
{

View File

@@ -0,0 +1,10 @@
namespace WeddingShare.Models
{
public class ExportOptions
{
public bool Database { get; set; } = true;
public bool Uploads { get; set; } = true;
public bool Thumbnails { get; set; } = true;
public bool CustomResources { get; set; } = true;
}
}

View File

@@ -1,18 +0,0 @@
namespace WeddingShare.Models
{
public class FileUploader
{
public FileUploader(string id, string? key, string url, bool identityRequired = false)
{
this.GalleryId = id;
this.SecretKey = key;
this.UploadUrl = url;
this.IdentityRequired = identityRequired;
}
public string? GalleryId { get; set; }
public string? SecretKey { get; set; }
public string? UploadUrl { get; set; }
public bool IdentityRequired { get; set; }
}
}

View File

@@ -1,32 +1,37 @@
using WeddingShare.Enums;
using WeddingShare.Models.Database;
namespace WeddingShare.Models
{
public class PhotoGallery
{
public PhotoGallery()
: this(ViewMode.Default)
: this(ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending)
{
}
public PhotoGallery(ViewMode viewMode)
: this(1, "default", string.Empty, viewMode, new List<PhotoGalleryImage>(), false)
public PhotoGallery(ViewMode viewMode, GalleryGroup groupBy, GalleryOrder orderBy)
: this(null, string.Empty, viewMode, groupBy, orderBy, new List<PhotoGalleryImage>(), false)
{
}
public PhotoGallery(int id, string name, string secretKey, ViewMode viewMode, List<PhotoGalleryImage> images, bool requireIdentity)
public PhotoGallery(GalleryModel? gallery, string secretKey, ViewMode viewMode, GalleryGroup groupBy, GalleryOrder orderBy, List<PhotoGalleryImage> images, bool uploadActivated)
{
this.GalleryId = id;
this.GalleryName = name;
this.Gallery = gallery;
this.SecretKey = secretKey;
this.ViewMode = viewMode;
this.GroupBy = groupBy;
this.OrderBy = orderBy;
this.PendingCount = 0;
this.Images = images;
this.FileUploader = new FileUploader(name, secretKey, "/Gallery/UploadImage", requireIdentity);
this.UploadActivated = uploadActivated;
}
public int? GalleryId { get; set; }
public string? GalleryName { get; set; }
public GalleryModel? Gallery { get; set; }
public string? SecretKey { get; set; }
public ViewMode ViewMode { get; set; }
public GalleryGroup GroupBy { get; set; }
public GalleryOrder OrderBy { get; set; }
public int ApprovedCount { get; set; }
public int PendingCount { get; set; }
public int ItemsPerPage { get; set; } = 50;
@@ -41,7 +46,7 @@ namespace WeddingShare.Models
}
}
public List<PhotoGalleryImage>? Images { get; set; }
public FileUploader? FileUploader { get; set; }
public bool UploadActivated { get; set; } = false;
}
public class PhotoGalleryImage
@@ -55,8 +60,10 @@ namespace WeddingShare.Models
public string? GalleryName { get; set; }
public string? Name { get; set; }
public string? UploadedBy { get; set; }
public DateTime? UploadDate { get; set; }
public string? ImagePath { get; set; }
public string? ThumbnailPath { get; set; }
public string? ThumbnailPathFallback { get; set; }
public MediaType MediaType { get; set; }
}
}

View File

@@ -7,5 +7,6 @@
public const string MultiFactorTokenSet = "2FA_SET";
public const string MultiFactorSecret = "2FA_SECRET";
public const string MultiFactorQR = "2FA_QR_CODE";
public const string SelectedLanguage = "SelectedLanguage";
}
}

View File

@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
namespace WeddingShare.Models
{
public class SponsorsList
{
[JsonPropertyName("tiers")]
public IEnumerable<SponsorsTier>? Tiers { get; set; }
}
public class SponsorsTier
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("priority")]
public int Priority { get; set; }
[JsonPropertyName("platforms")]
public IEnumerable<SponsorPlatform>? Platforms { get; set; }
}
public class SponsorPlatform
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("sponsors")]
public IEnumerable<string>? Sponsors { get; set; }
}
}

View File

@@ -0,0 +1,9 @@
namespace WeddingShare.Models
{
public class SupportedLanguage
{
public string Key { get; set; }
public string Value { get; set; }
public bool Selected { get; set; }
}
}

View File

@@ -0,0 +1,13 @@
using System.Runtime.Serialization;
namespace WeddingShare.Models
{
public class UpdateSettingsModel
{
[DataMember(Name = "key")]
public string Key { get; set; }
[DataMember(Name = "value")]
public string? Value { get; set; }
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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