global: add null safety checks

This commit is contained in:
Abdullah Atta
2025-10-14 21:15:51 +05:00
parent be432dfd24
commit 6e35edb715
109 changed files with 452 additions and 590 deletions
@@ -43,7 +43,7 @@ namespace Notesnook.API.Authorization
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
var result = this.IsAuthorized(context.User, path);
if (result.Succeeded) context.Succeed(requirement);
else if (result.AuthorizationFailure.FailureReasons.Any())
else if (result.AuthorizationFailure?.FailureReasons.Any() == true)
context.Fail(result.AuthorizationFailure.FailureReasons.First());
else context.Fail();
@@ -63,11 +63,11 @@ namespace Notesnook.API.Authorization
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
}
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
var isInAudience = User.HasClaim("aud", "notesnook");
var hasRole = User.HasClaim("role", "notesnook");
var hasSyncScope = User?.HasClaim("scope", "notesnook.sync") ?? false;
var isInAudience = User?.HasClaim("aud", "notesnook") ?? false;
var hasRole = User?.HasClaim("role", "notesnook") ?? false;
var isEmailVerified = User.HasClaim("verified", "true");
var isEmailVerified = User?.HasClaim("verified", "true") ?? false;
if (!isEmailVerified)
{
@@ -57,7 +57,7 @@ namespace Notesnook.API.Controllers
if (item.Type != "callToActions") continue;
foreach (var action in item.Actions)
{
if (action.Type != "link") continue;
if (action.Type != "link" || action.Data == null) continue;
action.Data = action.Data.Replace("{{UserId}}", userId ?? "0");
}
+5 -5
View File
@@ -47,7 +47,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> GetApiKeysAsync()
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
var apiKeys = await inboxApiKeysRepository.FindAsync(t => t.UserId == userId);
@@ -64,7 +64,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> CreateApiKeyAsync([FromBody] InboxApiKey request)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
if (string.IsNullOrWhiteSpace(request.Name))
@@ -104,7 +104,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> DeleteApiKeyAsync(string apiKey)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
if (string.IsNullOrWhiteSpace(apiKey))
@@ -126,7 +126,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetPublicKeyAsync()
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
@@ -147,7 +147,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> CreateInboxItemAsync([FromBody] InboxSyncItem request)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
if (request.Key.Algorithm != Algorithms.XSAL_X25519_7)
@@ -98,9 +98,8 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
var jti = this.User.FindFirstValue("jti");
if (userId == null) return Unauthorized();
var existingMonograph = await FindMonographAsync(userId, monograph);
if (existingMonograph != null && !existingMonograph.Deleted) return await UpdateAsync(deviceId, monograph);
@@ -144,9 +143,8 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
var jti = this.User.FindFirstValue("jti");
if (userId == null) return Unauthorized();
var existingMonograph = await FindMonographAsync(userId, monograph);
if (existingMonograph == null || existingMonograph.Deleted)
@@ -193,8 +191,7 @@ namespace Notesnook.API.Controllers
[HttpGet]
public async Task<IActionResult> GetUserMonographsAsync()
{
var userId = this.User.FindFirstValue("sub");
if (userId == null) return Unauthorized();
var userId = this.User.GetUserId();
var userMonographs = (await monographs.Collection.FindAsync(
Builders<Monograph>.Filter.And(
@@ -257,8 +254,7 @@ namespace Notesnook.API.Controllers
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync([FromQuery] string? deviceId, [FromRoute] string id)
{
var userId = this.User.FindFirstValue("sub");
if (userId is null) return Unauthorized();
var userId = this.User.GetUserId();
var monograph = await FindMonographAsync(id);
if (monograph == null || monograph.Deleted)
@@ -310,12 +306,13 @@ namespace Notesnook.API.Controllers
});
}
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string content)
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string? content)
{
if (string.IsNullOrEmpty(content)) return string.Empty;
if (Constants.IS_SELF_HOSTED) return content;
try
{
var json = JsonSerializer.Deserialize<MonographContent>(content);
var json = JsonSerializer.Deserialize<MonographContent>(content) ?? throw new Exception("Invalid monograph content.");
var html = json.Data;
if (user.IsUserSubscribed())
@@ -44,7 +44,7 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub") ?? throw new Exception("User not found.");
var userId = this.User.GetUserId();
new SyncDeviceService(new SyncDevice(userId, deviceId)).RegisterDevice();
return Ok();
}
@@ -61,7 +61,7 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub") ?? throw new Exception("User not found.");
var userId = this.User.GetUserId();
new SyncDeviceService(new SyncDevice(userId, deviceId)).UnregisterDevice();
return Ok();
}
+4 -4
View File
@@ -55,7 +55,7 @@ namespace Notesnook.API.Controllers
[HttpGet]
public async Task<IActionResult> GetUser()
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
UserResponse response = await UserService.GetUserAsync(userId);
@@ -72,7 +72,7 @@ namespace Notesnook.API.Controllers
[HttpPatch]
public async Task<IActionResult> UpdateUser([FromBody] UserKeys keys)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
await UserService.SetUserKeysAsync(userId, keys);
@@ -88,7 +88,7 @@ namespace Notesnook.API.Controllers
[HttpPost("reset")]
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
if (await UserService.ResetUserAsync(userId, removeAttachments))
return Ok();
@@ -99,7 +99,7 @@ namespace Notesnook.API.Controllers
[RequestTimeout(5 * 60 * 1000)]
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
var jti = User.FindFirstValue("jti");
try
{
@@ -10,5 +10,8 @@ namespace System.Security.Claims
private readonly static string[] SUBSCRIBED_CLAIMS = ["believer", "education", "essential", "pro", "legacy_pro"];
public static bool IsUserSubscribed(this ClaimsPrincipal user)
=> user.Claims.Any((c) => c.Type == "notesnook:status" && SUBSCRIBED_CLAIMS.Contains(c.Value));
public static string GetUserId(this ClaimsPrincipal user)
=> user.FindFirstValue("sub") ?? throw new Exception("User ID not found in claims.");
}
}
+2 -1
View File
@@ -7,7 +7,7 @@ namespace Notesnook.API.Jobs
{
public class DeviceCleanupJob : IJob
{
public async Task Execute(IJobExecutionContext context)
public Task Execute(IJobExecutionContext context)
{
ParallelOptions parallelOptions = new()
{
@@ -59,6 +59,7 @@ namespace Notesnook.API.Jobs
}
}
});
return Task.CompletedTask;
}
}
}
+21 -21
View File
@@ -40,7 +40,7 @@ namespace Notesnook.API.Models
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("timestamp")]
[BsonElement("timestamp")]
@@ -48,7 +48,7 @@ namespace Notesnook.API.Models
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
public required string[] Platforms { get; set; }
[JsonPropertyName("isActive")]
[BsonElement("isActive")]
@@ -56,7 +56,7 @@ namespace Notesnook.API.Models
[JsonPropertyName("userTypes")]
[BsonElement("userTypes")]
public string[] UserTypes { get; set; }
public required string[] UserTypes { get; set; }
[JsonPropertyName("appVersion")]
[BsonElement("appVersion")]
@@ -64,63 +64,63 @@ namespace Notesnook.API.Models
[JsonPropertyName("body")]
[BsonElement("body")]
public BodyComponent[] Body { get; set; }
public required BodyComponent[] Body { get; set; }
[JsonIgnore]
[BsonElement("userIds")]
public string[] UserIds { get; set; }
public string[]? UserIds { get; set; }
[Obsolete]
[JsonPropertyName("title")]
[DataMember(Name = "title")]
[BsonElement("title")]
public string Title { get; set; }
public string? Title { get; set; }
[Obsolete]
[JsonPropertyName("description")]
[BsonElement("description")]
public string Description { get; set; }
public string? Description { get; set; }
[Obsolete]
[JsonPropertyName("callToActions")]
[BsonElement("callToActions")]
public CallToAction[] CallToActions { get; set; }
public CallToAction[]? CallToActions { get; set; }
}
public class BodyComponent
{
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
public string[]? Platforms { get; set; }
[JsonPropertyName("style")]
[BsonElement("style")]
public Style Style { get; set; }
public Style? Style { get; set; }
[JsonPropertyName("src")]
[BsonElement("src")]
public string Src { get; set; }
public string? Src { get; set; }
[JsonPropertyName("text")]
[BsonElement("text")]
public string Text { get; set; }
public string? Text { get; set; }
[JsonPropertyName("value")]
[BsonElement("value")]
public string Value { get; set; }
public string? Value { get; set; }
[JsonPropertyName("items")]
[BsonElement("items")]
public BodyComponent[] Items { get; set; }
public BodyComponent[]? Items { get; set; }
[JsonPropertyName("actions")]
[BsonElement("actions")]
public CallToAction[] Actions { get; set; }
public required CallToAction[] Actions { get; set; }
}
public class Style
@@ -135,25 +135,25 @@ namespace Notesnook.API.Models
[JsonPropertyName("textAlign")]
[BsonElement("textAlign")]
public string TextAlign { get; set; }
public string? TextAlign { get; set; }
}
public class CallToAction
{
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
public string[]? Platforms { get; set; }
[JsonPropertyName("data")]
[BsonElement("data")]
public string Data { get; set; }
public string? Data { get; set; }
[JsonPropertyName("title")]
[BsonElement("title")]
public string Title { get; set; }
public string? Title { get; set; }
}
}
@@ -5,9 +5,9 @@ namespace Notesnook.API.Models;
public class CompleteMultipartUploadRequestWrapper
{
public string Key { get; set; }
public List<PartETagWrapper> PartETags { get; set; }
public string UploadId { get; set; }
public required string Key { get; set; }
public required List<PartETagWrapper> PartETags { get; set; }
public required string UploadId { get; set; }
public CompleteMultipartUploadRequest ToRequest()
{
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Notesnook.API.Models
public class DeleteAccountForm
{
[Required]
public string Password
public required string Password
{
get; set;
}
+5 -11
View File
@@ -26,25 +26,19 @@ using System.Text.Json.Serialization;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class EncryptedData : IEncrypted
public class EncryptedData
{
[MessagePack.Key("iv")]
[JsonPropertyName("iv")]
[BsonElement("iv")]
[DataMember(Name = "iv")]
public string IV
{
get; set;
}
public required string IV { get; set; }
[MessagePack.Key("cipher")]
[JsonPropertyName("cipher")]
[BsonElement("cipher")]
[DataMember(Name = "cipher")]
public string Cipher
{
get; set;
}
public required string Cipher { get; set; }
[MessagePack.Key("length")]
[JsonPropertyName("length")]
@@ -56,9 +50,9 @@ namespace Notesnook.API.Models
[JsonPropertyName("salt")]
[BsonElement("salt")]
[DataMember(Name = "salt")]
public string Salt { get; set; }
public required string Salt { get; set; }
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
if (obj is EncryptedData encryptedData)
{
+4 -4
View File
@@ -37,16 +37,16 @@ namespace Notesnook.API.Models
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id { get; set; }
public string Id { get; set; } = string.Empty;
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
public required string Name { get; set; }
[JsonPropertyName("key")]
public string Key { get; set; }
public string Key { get; set; } = string.Empty;
[JsonPropertyName("dateCreated")]
public long DateCreated { get; set; }
+4 -16
View File
@@ -31,19 +31,13 @@ namespace Notesnook.API.Models
[JsonPropertyName("key")]
[MessagePack.Key("key")]
[Required]
public EncryptedKey Key
{
get; set;
}
public required EncryptedKey Key { get; set; }
[DataMember(Name = "salt")]
[JsonPropertyName("salt")]
[MessagePack.Key("salt")]
[Required]
public string Salt
{
get; set;
}
public required string Salt { get; set; }
}
[MessagePack.MessagePackObject]
@@ -53,19 +47,13 @@ namespace Notesnook.API.Models
[JsonPropertyName("alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
}
public required string Algorithm { get; set; }
[DataMember(Name = "cipher")]
[JsonPropertyName("cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
get; set;
}
public required string Cipher { get; set; }
[JsonPropertyName("length")]
[DataMember(Name = "length")]
+5 -17
View File
@@ -29,15 +29,9 @@ namespace Notesnook.API.Models
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public string Id
{
get; set;
}
public required string Id { get; set; }
public string ItemId
{
get; set;
}
public required string ItemId { get; set; }
}
public class Monograph
@@ -50,23 +44,17 @@ namespace Notesnook.API.Models
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public string ItemId
{
get; set;
}
public string? ItemId { get; set; }
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id
{
get; set;
}
public string Id { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; }
public string? Title { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
+2 -2
View File
@@ -28,8 +28,8 @@ namespace Notesnook.API.Models
public class MonographContent
{
[JsonPropertyName("data")]
public string Data { get; set; }
public required string Data { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
public required string Type { get; set; }
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ namespace Notesnook.API.Models
}
[JsonPropertyName("title")]
public required string Title { get; set; }
public string? Title { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
+4 -2
View File
@@ -17,11 +17,13 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
namespace Notesnook.API.Models
{
public class MultipartUploadMeta
{
public string UploadId { get; set; }
public string[] Parts { get; set; }
public string UploadId { get; set; } = string.Empty;
public string[] Parts { get; set; } = Array.Empty<string>();
}
}
+1 -1
View File
@@ -3,5 +3,5 @@
public class PartETagWrapper
{
public int PartNumber { get; set; }
public string ETag { get; set; }
public string ETag { get; set; } = string.Empty;
}
@@ -6,9 +6,9 @@ namespace Notesnook.API.Models.Responses
public class SignupResponse : Response
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public string? UserId { get; set; }
[JsonPropertyName("errors")]
public string[] Errors { get; set; }
public string[]? Errors { get; set; }
}
}
+4 -4
View File
@@ -21,9 +21,9 @@ namespace Notesnook.API.Models
{
public class S3Options
{
public string ServiceUrl { get; set; }
public string Region { get; set; }
public string AccessKeyId { get; set; }
public string SecretAccessKey { get; set; }
public string ServiceUrl { get; set; } = string.Empty;
public string Region { get; set; } = string.Empty;
public string AccessKeyId { get; set; } = string.Empty;
public string SecretAccessKey { get; set; } = string.Empty;
}
}
+3 -12
View File
@@ -53,20 +53,14 @@ namespace Notesnook.API.Models
[DataMember(Name = "iv")]
[MessagePack.Key("iv")]
[Required]
public string IV
{
get; set;
}
public string IV { get; set; } = string.Empty;
[JsonPropertyName("cipher")]
[DataMember(Name = "cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
get; set;
}
public string Cipher { get; set; } = string.Empty;
[DataMember(Name = "id")]
[JsonPropertyName("id")]
@@ -108,10 +102,7 @@ namespace Notesnook.API.Models
[DataMember(Name = "alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
}
public string Algorithm { get; set; } = string.Empty;
}
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
+6 -6
View File
@@ -29,23 +29,23 @@ namespace Notesnook.API.Models
public long UpdatedAt { get; set; }
}
public class UserSettings : IUserSettings
public class UserSettings
{
public UserSettings()
{
this.Id = ObjectId.GenerateNewId().ToString();
this.Id = ObjectId.GenerateNewId();
}
public string UserId { get; set; }
public required string UserId { get; set; }
public long LastSynced { get; set; }
public string Salt { get; set; }
public required string Salt { get; set; }
public EncryptedData? VaultKey { get; set; }
public EncryptedData? AttachmentsKey { get; set; }
public EncryptedData? MonographPasswordsKey { get; set; }
public InboxKeys? InboxKeys { get; set; }
public Limit StorageLimit { get; set; }
public Limit? StorageLimit { get; set; }
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public ObjectId Id { get; set; }
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ namespace Notesnook.API
{
options.Limits.MaxRequestBodySize = long.MaxValue;
options.ListenAnyIP(Servers.NotesnookAPI.Port);
if (Servers.NotesnookAPI.IsSecure)
if (Servers.NotesnookAPI.IsSecure && Servers.NotesnookAPI.SSLCertificate != null)
{
options.ListenAnyIP(443, listenerOptions =>
{
+3 -3
View File
@@ -52,7 +52,7 @@ namespace Notesnook.API.Services
public async Task CreateUserAsync()
{
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
if (!response.Success || (response.Errors != null && response.Errors.Length > 0))
if (!response.Success || (response.Errors != null && response.Errors.Length > 0) || response.UserId == null)
{
logger.LogError("Failed to sign up user: {Response}", JsonSerializer.Serialize(response));
if (response.Errors != null && response.Errors.Length > 0)
@@ -216,7 +216,7 @@ namespace Notesnook.API.Services
await S3Service.DeleteDirectoryAsync(userId);
}
public async Task DeleteUserAsync(string userId, string jti, string password)
public async Task DeleteUserAsync(string userId, string? jti, string password)
{
logger.LogInformation("Deleting user account: {UserId}", userId);
@@ -227,7 +227,7 @@ namespace Notesnook.API.Services
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
SendToAll = false,
SendToAll = jti == null,
OriginTokenId = jti,
UserId = userId,
Message = new Message
+5 -10
View File
@@ -119,8 +119,8 @@ namespace Notesnook.API
policy.RequireAuthenticatedUser();
});
options.DefaultPolicy = options.GetPolicy("Notesnook");
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>(); ;
options.DefaultPolicy = options.GetPolicy("Notesnook") ?? throw new Exception("Notesnook policy not found");
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddOAuth2Introspection("introspection", options =>
@@ -138,13 +138,13 @@ namespace Notesnook.API
options.Events.OnTokenValidated = (context) =>
{
if (long.TryParse(context.Principal.FindFirst("exp")?.Value, out long expiryTime))
if (long.TryParse(context.Principal?.FindFirst("exp")?.Value, out long expiryTime))
{
context.Properties.ExpiresUtc = DateTimeOffset.FromUnixTimeSeconds(expiryTime);
}
context.Properties.AllowRefresh = true;
context.Properties.IsPersistent = true;
context.HttpContext.User = context.Principal;
context.HttpContext.User = context.Principal ?? throw new Exception("No principal found in token.");
return Task.CompletedTask;
};
options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256();
@@ -289,11 +289,6 @@ namespace Notesnook.API
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
endpoints.MapHub<SyncHub>("/hubs/sync", options =>
{
options.CloseOnAuthenticationExpiration = false;
options.Transports = HttpTransportType.WebSockets;
});
endpoints.MapHub<SyncV2Hub>("/hubs/sync/v2", options =>
{
options.CloseOnAuthenticationExpiration = false;
@@ -307,7 +302,7 @@ namespace Notesnook.API
{
public static IServiceCollection AddMongoCollection(this IServiceCollection services, string collectionName, string database = "notesnook")
{
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
return services;
}
}
@@ -1,33 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Text.Json.Serialization;
namespace Streetwriters.Common.Attributes
{
[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false)]
public class JsonInterfaceConverterAttribute : JsonConverterAttribute
{
public JsonInterfaceConverterAttribute(Type converterType)
: base(converterType)
{
}
}
}
+5 -4
View File
@@ -33,7 +33,7 @@ namespace Streetwriters.Common
{
Id = "notesnook",
Name = "Notesnook",
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL,
SenderEmail = Constants.NOTESNOOK_SENDER_EMAIL ?? "noreply@notesnook.com",
SenderName = "Notesnook",
Type = ApplicationType.NOTESNOOK,
AppId = ApplicationType.NOTESNOOK,
@@ -58,14 +58,15 @@ namespace Streetwriters.Common
{ "notesnook", Notesnook }
};
public static Client FindClientById(string id)
public static Client? FindClientById(string? id)
{
if (!IsValidClient(id)) return null;
if (string.IsNullOrEmpty(id) || !IsValidClient(id)) return null;
return ClientsMap[id];
}
public static Client FindClientByAppId(ApplicationType appId)
public static Client? FindClientByAppId(ApplicationType? appId)
{
if (appId is null) return null;
switch (appId)
{
case ApplicationType.NOTESNOOK:
+34 -34
View File
@@ -29,54 +29,54 @@ namespace Streetwriters.Common
public static string INSTANCE_NAME => Environment.GetEnvironmentVariable("INSTANCE_NAME") ?? "default";
// S3 related
public static string S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
public static string S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
public static string S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
public static string S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
public static string S3_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_BUCKET_NAME");
public static string S3_INTERNAL_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_INTERNAL_BUCKET_NAME");
public static string S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
public static string? S3_ACCESS_KEY => Environment.GetEnvironmentVariable("S3_ACCESS_KEY");
public static string? S3_ACCESS_KEY_ID => Environment.GetEnvironmentVariable("S3_ACCESS_KEY_ID");
public static string? S3_SERVICE_URL => Environment.GetEnvironmentVariable("S3_SERVICE_URL");
public static string? S3_REGION => Environment.GetEnvironmentVariable("S3_REGION");
public static string? S3_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_BUCKET_NAME");
public static string? S3_INTERNAL_BUCKET_NAME => Environment.GetEnvironmentVariable("S3_INTERNAL_BUCKET_NAME");
public static string? S3_INTERNAL_SERVICE_URL => Environment.GetEnvironmentVariable("S3_INTERNAL_SERVICE_URL");
// SMTP settings
public static string SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
public static string SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
public static string SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
public static string SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
public static string NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL") ?? Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string? SMTP_USERNAME => Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string? SMTP_PASSWORD => Environment.GetEnvironmentVariable("SMTP_PASSWORD");
public static string? SMTP_HOST => Environment.GetEnvironmentVariable("SMTP_HOST");
public static string? SMTP_PORT => Environment.GetEnvironmentVariable("SMTP_PORT");
public static string? SMTP_REPLYTO_EMAIL => Environment.GetEnvironmentVariable("SMTP_REPLYTO_EMAIL");
public static string? NOTESNOOK_SENDER_EMAIL => Environment.GetEnvironmentVariable("NOTESNOOK_SENDER_EMAIL") ?? Environment.GetEnvironmentVariable("SMTP_USERNAME");
public static string NOTESNOOK_APP_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_APP_HOST");
public static string NOTESNOOK_API_SECRET => Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET");
public static string? NOTESNOOK_APP_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_APP_HOST");
public static string NOTESNOOK_API_SECRET => Environment.GetEnvironmentVariable("NOTESNOOK_API_SECRET") ?? throw new InvalidOperationException("NOTESNOOK_API_SECRET is required");
// MessageBird is used for SMS sending
public static string TWILIO_ACCOUNT_SID => Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
public static string TWILIO_AUTH_TOKEN => Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
public static string TWILIO_SERVICE_SID => Environment.GetEnvironmentVariable("TWILIO_SERVICE_SID");
public static string? TWILIO_ACCOUNT_SID => Environment.GetEnvironmentVariable("TWILIO_ACCOUNT_SID");
public static string? TWILIO_AUTH_TOKEN => Environment.GetEnvironmentVariable("TWILIO_AUTH_TOKEN");
public static string? TWILIO_SERVICE_SID => Environment.GetEnvironmentVariable("TWILIO_SERVICE_SID");
// Server discovery
public static int NOTESNOOK_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_PORT") ?? "80");
public static string NOTESNOOK_SERVER_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_HOST");
public static string NOTESNOOK_CERT_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_PATH");
public static string NOTESNOOK_CERT_KEY_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_KEY_PATH");
public static string? NOTESNOOK_SERVER_HOST => Environment.GetEnvironmentVariable("NOTESNOOK_SERVER_HOST");
public static string? NOTESNOOK_CERT_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_PATH");
public static string? NOTESNOOK_CERT_KEY_PATH => Environment.GetEnvironmentVariable("NOTESNOOK_CERT_KEY_PATH");
public static int IDENTITY_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("IDENTITY_SERVER_PORT") ?? "80");
public static string IDENTITY_SERVER_HOST => Environment.GetEnvironmentVariable("IDENTITY_SERVER_HOST");
public static Uri IDENTITY_SERVER_URL => new(Environment.GetEnvironmentVariable("IDENTITY_SERVER_URL"));
public static string IDENTITY_CERT_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_PATH");
public static string IDENTITY_CERT_KEY_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_KEY_PATH");
public static string? IDENTITY_SERVER_HOST => Environment.GetEnvironmentVariable("IDENTITY_SERVER_HOST");
public static Uri? IDENTITY_SERVER_URL => Environment.GetEnvironmentVariable("IDENTITY_SERVER_URL") is string url ? new Uri(url) : null;
public static string? IDENTITY_CERT_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_PATH");
public static string? IDENTITY_CERT_KEY_PATH => Environment.GetEnvironmentVariable("IDENTITY_CERT_KEY_PATH");
public static int SSE_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SSE_SERVER_PORT") ?? "80");
public static string SSE_SERVER_HOST => Environment.GetEnvironmentVariable("SSE_SERVER_HOST");
public static string SSE_CERT_PATH => Environment.GetEnvironmentVariable("SSE_CERT_PATH");
public static string SSE_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SSE_CERT_KEY_PATH");
public static string? SSE_SERVER_HOST => Environment.GetEnvironmentVariable("SSE_SERVER_HOST");
public static string? SSE_CERT_PATH => Environment.GetEnvironmentVariable("SSE_CERT_PATH");
public static string? SSE_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SSE_CERT_KEY_PATH");
// internal
public static string WEBRISK_API_URI => Environment.GetEnvironmentVariable("WEBRISK_API_URI");
public static string MONGODB_CONNECTION_STRING => Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING");
public static string MONGODB_DATABASE_NAME => Environment.GetEnvironmentVariable("MONGODB_DATABASE_NAME");
public static string? WEBRISK_API_URI => Environment.GetEnvironmentVariable("WEBRISK_API_URI");
public static string MONGODB_CONNECTION_STRING => Environment.GetEnvironmentVariable("MONGODB_CONNECTION_STRING") ?? throw new ArgumentNullException("MONGODB_CONNECTION_STRING environment variable is not set");
public static string MONGODB_DATABASE_NAME => Environment.GetEnvironmentVariable("MONGODB_DATABASE_NAME") ?? throw new ArgumentNullException("MONGODB_DATABASE_NAME environment variable is not set");
public static int SUBSCRIPTIONS_SERVER_PORT => int.Parse(Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_PORT") ?? "80");
public static string SUBSCRIPTIONS_SERVER_HOST => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_HOST");
public static string SUBSCRIPTIONS_CERT_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_PATH");
public static string SUBSCRIPTIONS_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_KEY_PATH");
public static string? SUBSCRIPTIONS_SERVER_HOST => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_SERVER_HOST");
public static string? SUBSCRIPTIONS_CERT_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_PATH");
public static string? SUBSCRIPTIONS_CERT_KEY_PATH => Environment.GetEnvironmentVariable("SUBSCRIPTIONS_CERT_KEY_PATH");
public static string[] NOTESNOOK_CORS_ORIGINS => Environment.GetEnvironmentVariable("NOTESNOOK_CORS")?.Split(",") ?? new string[] { };
}
}
@@ -1,54 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Streetwriters.Common.Converters
{
/// <summary>
/// Converts simple interface into an object (assumes that there is only one class of TInterface)
/// </summary>
/// <typeparam name="TInterface">Interface type</typeparam>
/// <typeparam name="TClass">Class type</typeparam>
public class InterfaceConverter<TInterface, TClass> : JsonConverter<TInterface> where TClass : TInterface
{
public override TInterface Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return JsonSerializer.Deserialize<TClass>(ref reader, options);
}
public override void Write(Utf8JsonWriter writer, TInterface value, JsonSerializerOptions options)
{
switch (value)
{
case null:
JsonSerializer.Serialize(writer, null, options);
break;
default:
{
var type = value.GetType();
JsonSerializer.Serialize(writer, value, type, options);
break;
}
}
}
}
}
@@ -42,7 +42,7 @@ namespace Streetwriters.Common.Extensions
var data = new Dictionary<string, object>
{
{ "version", Constants.COMPATIBILITY_VERSION },
{ "id", server.Id },
{ "id", server.Id ?? "unknown" },
{ "instance", Constants.INSTANCE_NAME }
};
await context.Response.WriteAsync(JsonSerializer.Serialize(data));
@@ -70,12 +70,12 @@ namespace Streetwriters.Common.Extensions
return app;
}
public static T GetService<T>(this IApplicationBuilder app)
public static T GetService<T>(this IApplicationBuilder app) where T : notnull
{
return app.ApplicationServices.GetRequiredService<T>();
}
public static T GetScopedService<T>(this IApplicationBuilder app)
public static T GetScopedService<T>(this IApplicationBuilder app) where T : notnull
{
using (var scope = app.ApplicationServices.CreateScope())
{
@@ -17,6 +17,7 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
@@ -28,7 +29,7 @@ namespace Streetwriters.Common.Extensions
{
public static class HttpClientExtensions
{
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary headers, HttpMethod method, HttpContent content = null) where T : IResponse, new()
public static async Task<T> SendRequestAsync<T>(this HttpClient httpClient, string url, IHeaderDictionary? headers, HttpMethod method, HttpContent? content = null) where T : IResponse, new()
{
var request = new HttpRequestMessage(method, url);
@@ -51,22 +52,23 @@ namespace Streetwriters.Common.Extensions
}
var response = await httpClient.SendAsync(request);
if (response.Content.Headers.ContentLength > 0 && response.Content.Headers.ContentType.ToString().Contains("application/json"))
if (response.Content.Headers.ContentLength > 0 && response.Content.Headers.ContentType?.ToString()?.Contains("application/json") == true)
{
var res = await response.Content.ReadFromJsonAsync<T>();
res.Success = response.IsSuccessStatusCode;
res.StatusCode = (int)response.StatusCode;
return res;
}
else
{
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content };
if (res != null)
{
res.Success = response.IsSuccessStatusCode;
res.StatusCode = (int)response.StatusCode;
return res;
}
}
return new T { Success = response.IsSuccessStatusCode, StatusCode = (int)response.StatusCode, Content = response.Content };
}
public static Task<T> ForwardAsync<T>(this HttpClient httpClient, IHttpContextAccessor accessor, string url, HttpMethod method) where T : IResponse, new()
{
var httpContext = accessor.HttpContext;
var httpContext = accessor.HttpContext ?? throw new InvalidOperationException("HttpContext is not available");
var content = new StreamContent(httpContext.Request.BodyReader.AsStream());
return httpClient.SendRequestAsync<T>(url, httpContext.Request.Headers, method, content);
}
@@ -27,7 +27,7 @@ namespace Streetwriters.Common.Extensions
{
public static IServiceCollection AddRepository<T>(this IServiceCollection services, string collectionName, string database) where T : class
{
services.AddSingleton((provider) => MongoDbContext.GetMongoCollection<T>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddSingleton((provider) => MongoDbContext.GetMongoCollection<T>(provider.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddScoped<Repository<T>>();
return services;
}
+1 -1
View File
@@ -33,6 +33,6 @@ namespace Streetwriters.Common.Interfaces
string SenderName { get; set; }
string EmailConfirmedRedirectURL { get; }
string AccountRecoveryRedirectURL { get; }
Func<string, Task> OnEmailConfirmed { get; set; }
Func<string, Task>? OnEmailConfirmed { get; set; }
}
}
+3 -1
View File
@@ -17,11 +17,13 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using MongoDB.Bson;
namespace Streetwriters.Common.Interfaces
{
public interface IDocument
{
string Id
ObjectId Id
{
get; set;
}
@@ -12,8 +12,8 @@ namespace Streetwriters.Common.Interfaces
string email,
EmailTemplate template,
IClient client,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
GnuPGContext? gpgContext = null,
Dictionary<string, byte[]>? attachments = null
);
}
}
-32
View File
@@ -1,32 +0,0 @@
/*
This file is part of the Notesnook Sync Server project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the Affero GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Affero GNU General Public License for more details.
You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Collections.Generic;
using Streetwriters.Common.Enums;
using Streetwriters.Common.Models;
namespace Streetwriters.Common.Interfaces
{
public interface IOffer : IDocument
{
ApplicationType AppId { get; set; }
string PromoCode { get; set; }
PromoCode[] Codes { get; set; }
}
}
+1 -1
View File
@@ -25,6 +25,6 @@ namespace Streetwriters.Common.Interfaces
{
bool Success { get; set; }
int StatusCode { get; set; }
HttpContent Content { get; set; }
HttpContent? Content { get; set; }
}
}
@@ -8,7 +8,7 @@ namespace Streetwriters.Common.Interfaces
public interface IUserSubscriptionService
{
[WampProcedure("co.streetwriters.subscriptions.subscriptions.get_user_subscription")]
Task<Subscription> GetUserSubscriptionAsync(string clientId, string userId);
Task<Subscription?> GetUserSubscriptionAsync(string clientId, string userId);
Subscription TransformUserSubscription(Subscription subscription);
}
}
@@ -28,7 +28,7 @@ namespace Streetwriters.Common.Messages
public class CreateSubscriptionMessage
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("provider")]
public SubscriptionProvider Provider { get; set; }
@@ -46,19 +46,19 @@ namespace Streetwriters.Common.Messages
public long ExpiryTime { get; set; }
[JsonPropertyName("orderId")]
public string OrderId { get; set; }
public string? OrderId { get; set; }
[JsonPropertyName("updateURL")]
public string UpdateURL { get; set; }
public string? UpdateURL { get; set; }
[JsonPropertyName("cancelURL")]
public string CancelURL { get; set; }
public string? CancelURL { get; set; }
[JsonPropertyName("subscriptionId")]
public string SubscriptionId { get; set; }
public string? SubscriptionId { get; set; }
[JsonPropertyName("productId")]
public string ProductId { get; set; }
public string? ProductId { get; set; }
[JsonPropertyName("extend")]
public bool Extend { get; set; }
@@ -28,7 +28,7 @@ namespace Streetwriters.Common.Messages
public class CreateSubscriptionMessageV2
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("provider")]
public SubscriptionProvider Provider { get; set; }
@@ -49,13 +49,13 @@ namespace Streetwriters.Common.Messages
public long ExpiryTime { get; set; }
[JsonPropertyName("orderId")]
public string OrderId { get; set; }
public string? OrderId { get; set; }
[JsonPropertyName("subscriptionId")]
public string SubscriptionId { get; set; }
public string? SubscriptionId { get; set; }
[JsonPropertyName("productId")]
public string ProductId { get; set; }
public string? ProductId { get; set; }
[JsonPropertyName("timestamp")]
public long Timestamp { get; set; }
@@ -27,7 +27,7 @@ namespace Streetwriters.Common.Messages
public class DeleteSubscriptionMessage
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("appId")]
public ApplicationType AppId { get; set; }
@@ -27,6 +27,6 @@ namespace Streetwriters.Common.Messages
public class DeleteUserMessage
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
}
}
@@ -26,10 +26,10 @@ namespace Streetwriters.Common.Messages
public class Message
{
[JsonPropertyName("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; }
public string? Data { get; set; }
}
public class SendSSEMessage
{
@@ -37,10 +37,10 @@ namespace Streetwriters.Common.Messages
public bool SendToAll { get; set; }
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("message")]
public Message Message { get; set; }
public required Message Message { get; set; }
[JsonPropertyName("originTokenId")]
public string? OriginTokenId { get; set; }
+7 -7
View File
@@ -31,15 +31,15 @@ namespace Streetwriters.Common.Models
{
public class Client : IClient
{
public string Id { get; set; }
public string Name { get; set; }
public required string Id { get; set; }
public required string Name { get; set; }
public ApplicationType Type { get; set; }
public ApplicationType AppId { get; set; }
public string SenderEmail { get; set; }
public string SenderName { get; set; }
public string EmailConfirmedRedirectURL { get; set; }
public string AccountRecoveryRedirectURL { get; set; }
public required string SenderEmail { get; set; }
public required string SenderName { get; set; }
public required string EmailConfirmedRedirectURL { get; set; }
public required string AccountRecoveryRedirectURL { get; set; }
public Func<string, Task> OnEmailConfirmed { get; set; }
public Func<string, Task>? OnEmailConfirmed { get; set; }
}
}
+4 -4
View File
@@ -3,9 +3,9 @@ namespace Streetwriters.Common.Models
public class EmailTemplate
{
public int? Id { get; set; }
public object Data { get; set; }
public string Subject { get; set; }
public string Html { get; set; }
public string Text { get; set; }
public object? Data { get; set; }
public required string Subject { get; set; }
public required string Html { get; set; }
public required string Text { get; set; }
}
}
@@ -10,12 +10,12 @@ namespace Streetwriters.Common.Models
public partial class GetCustomerResponse : PaddleResponse
{
[JsonPropertyName("data")]
public PaddleCustomer Customer { get; set; }
public PaddleCustomer? Customer { get; set; }
}
public class PaddleCustomer
{
[JsonPropertyName("email")]
public string Email { get; set; }
public string? Email { get; set; }
}
}
@@ -10,7 +10,7 @@ namespace Streetwriters.Common.Models
public partial class GetSubscriptionResponse : PaddleResponse
{
[JsonPropertyName("data")]
public Data Data { get; set; }
public Data? Data { get; set; }
}
public partial class Data
@@ -22,7 +22,7 @@ namespace Streetwriters.Common.Models
// public string Status { get; set; }
[JsonPropertyName("customer_id")]
public string CustomerId { get; set; }
public string? CustomerId { get; set; }
// [JsonPropertyName("address_id")]
// public string AddressId { get; set; }
@@ -64,7 +64,7 @@ namespace Streetwriters.Common.Models
// public CurrentBillingPeriod CurrentBillingPeriod { get; set; }
[JsonPropertyName("billing_cycle")]
public BillingCycle BillingCycle { get; set; }
public BillingCycle? BillingCycle { get; set; }
// [JsonPropertyName("scheduled_change")]
// public object ScheduledChange { get; set; }
@@ -76,7 +76,7 @@ namespace Streetwriters.Common.Models
// public object CustomData { get; set; }
[JsonPropertyName("management_urls")]
public ManagementUrls ManagementUrls { get; set; }
public ManagementUrls? ManagementUrls { get; set; }
// [JsonPropertyName("discount")]
// public object Discount { get; set; }
@@ -91,7 +91,7 @@ namespace Streetwriters.Common.Models
public long Frequency { get; set; }
[JsonPropertyName("interval")]
public string Interval { get; set; }
public string? Interval { get; set; }
}
// public partial class CurrentBillingPeriod
@@ -206,9 +206,9 @@ namespace Streetwriters.Common.Models
public partial class ManagementUrls
{
[JsonPropertyName("update_payment_method")]
public Uri UpdatePaymentMethod { get; set; }
public Uri? UpdatePaymentMethod { get; set; }
[JsonPropertyName("cancel")]
public Uri Cancel { get; set; }
public Uri? Cancel { get; set; }
}
}
@@ -10,12 +10,12 @@ namespace Streetwriters.Common.Models
public class GetTransactionInvoiceResponse : PaddleResponse
{
[JsonPropertyName("data")]
public Invoice Invoice { get; set; }
public Invoice? Invoice { get; set; }
}
public partial class Invoice
{
[JsonPropertyName("url")]
public string Url { get; set; }
public string? Url { get; set; }
}
}
@@ -10,6 +10,6 @@ namespace Streetwriters.Common.Models
public partial class GetTransactionResponse : PaddleResponse
{
[JsonPropertyName("data")]
public TransactionV2 Transaction { get; set; }
public TransactionV2? Transaction { get; set; }
}
}
+7 -7
View File
@@ -9,14 +9,14 @@ namespace Streetwriters.Common.Models
{
public GiftCard()
{
Id = ObjectId.GenerateNewId().ToString();
Id = ObjectId.GenerateNewId();
}
public string Code { get; set; }
public string OrderId { get; set; }
public string OrderIdType { get; set; }
public string ProductId { get; set; }
public string RedeemedBy { get; set; }
public required string Code { get; set; }
public required string OrderId { get; set; }
public required string OrderIdType { get; set; }
public required string ProductId { get; set; }
public string? RedeemedBy { get; set; }
public long RedeemedAt { get; set; }
public long Timestamp { get; set; }
public long Term { get; set; }
@@ -24,6 +24,6 @@ namespace Streetwriters.Common.Models
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
public string Id { get; set; }
public ObjectId Id { get; set; }
}
}
@@ -9,7 +9,7 @@ namespace Streetwriters.Common.Models
public bool Success { get; set; }
[JsonPropertyName("response")]
public Payment[] Payments { get; set; }
public Payment[]? Payments { get; set; }
}
public partial class Payment
@@ -24,10 +24,10 @@ namespace Streetwriters.Common.Models
public double Amount { get; set; }
[JsonPropertyName("currency")]
public string Currency { get; set; }
public string? Currency { get; set; }
[JsonPropertyName("payout_date")]
public string PayoutDate { get; set; }
public string? PayoutDate { get; set; }
[JsonPropertyName("is_paid")]
public short IsPaid { get; set; }
@@ -36,6 +36,6 @@ namespace Streetwriters.Common.Models
public bool IsOneOffCharge { get; set; }
[JsonPropertyName("receipt_url")]
public string ReceiptUrl { get; set; }
public string? ReceiptUrl { get; set; }
}
}
@@ -9,31 +9,31 @@ namespace Streetwriters.Common.Models
public bool Success { get; set; }
[JsonPropertyName("response")]
public Transaction[] Transactions { get; set; }
public Transaction[]? Transactions { get; set; }
}
public partial class Transaction
{
[JsonPropertyName("order_id")]
public string OrderId { get; set; }
public string? OrderId { get; set; }
[JsonPropertyName("checkout_id")]
public string CheckoutId { get; set; }
public string? CheckoutId { get; set; }
[JsonPropertyName("amount")]
public string Amount { get; set; }
public string? Amount { get; set; }
[JsonPropertyName("currency")]
public string Currency { get; set; }
public string? Currency { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
public string? Status { get; set; }
[JsonPropertyName("created_at")]
public string CreatedAt { get; set; }
public string? CreatedAt { get; set; }
[JsonPropertyName("passthrough")]
public object Passthrough { get; set; }
public object? Passthrough { get; set; }
[JsonPropertyName("product_id")]
public long ProductId { get; set; }
@@ -45,13 +45,13 @@ namespace Streetwriters.Common.Models
public bool IsOneOff { get; set; }
[JsonPropertyName("subscription")]
public PaddleSubscription Subscription { get; set; }
public PaddleSubscription? Subscription { get; set; }
[JsonPropertyName("user")]
public PaddleTransactionUser User { get; set; }
public PaddleTransactionUser? User { get; set; }
[JsonPropertyName("receipt_url")]
public string ReceiptUrl { get; set; }
public string? ReceiptUrl { get; set; }
}
public partial class PaddleSubscription
@@ -60,7 +60,7 @@ namespace Streetwriters.Common.Models
public long SubscriptionId { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
public string? Status { get; set; }
}
public partial class PaddleTransactionUser
@@ -69,7 +69,7 @@ namespace Streetwriters.Common.Models
public long UserId { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
public string? Email { get; set; }
[JsonPropertyName("marketing_consent")]
public bool MarketingConsent { get; set; }
@@ -10,19 +10,19 @@ namespace Streetwriters.Common.Models
public partial class ListTransactionsResponseV2 : PaddleResponse
{
[JsonPropertyName("data")]
public TransactionV2[] Transactions { get; set; }
public TransactionV2[]? Transactions { get; set; }
}
public partial class TransactionV2
{
[JsonPropertyName("id")]
public string Id { get; set; }
public string? Id { get; set; }
[JsonPropertyName("status")]
public string Status { get; set; }
public string? Status { get; set; }
[JsonPropertyName("customer_id")]
public string CustomerId { get; set; }
public string? CustomerId { get; set; }
// [JsonPropertyName("address_id")]
// public string AddressId { get; set; }
@@ -31,10 +31,10 @@ namespace Streetwriters.Common.Models
// public object BusinessId { get; set; }
[JsonPropertyName("custom_data")]
public Dictionary<string, string> CustomData { get; set; }
public Dictionary<string, string>? CustomData { get; set; }
[JsonPropertyName("origin")]
public string Origin { get; set; }
public string? Origin { get; set; }
// [JsonPropertyName("collection_mode")]
// public string CollectionMode { get; set; }
@@ -49,10 +49,10 @@ namespace Streetwriters.Common.Models
// public string InvoiceNumber { get; set; }
[JsonPropertyName("billing_details")]
public BillingDetails BillingDetails { get; set; }
public BillingDetails? BillingDetails { get; set; }
[JsonPropertyName("billing_period")]
public BillingPeriod BillingPeriod { get; set; }
public BillingPeriod? BillingPeriod { get; set; }
// [JsonPropertyName("currency_code")]
// public string CurrencyCode { get; set; }
@@ -70,10 +70,10 @@ namespace Streetwriters.Common.Models
public DateTimeOffset? BilledAt { get; set; }
[JsonPropertyName("items")]
public Item[] Items { get; set; }
public Item[]? Items { get; set; }
[JsonPropertyName("details")]
public Details Details { get; set; }
public Details? Details { get; set; }
// [JsonPropertyName("payments")]
// public Payment[] Payments { get; set; }
@@ -88,7 +88,7 @@ namespace Streetwriters.Common.Models
// public bool EnableCheckout { get; set; }
[JsonPropertyName("payment_terms")]
public PaymentTerms PaymentTerms { get; set; }
public PaymentTerms? PaymentTerms { get; set; }
// [JsonPropertyName("purchase_order_number")]
// public string PurchaseOrderNumber { get; set; }
@@ -100,7 +100,7 @@ namespace Streetwriters.Common.Models
public partial class PaymentTerms
{
[JsonPropertyName("interval")]
public string Interval { get; set; }
public string? Interval { get; set; }
[JsonPropertyName("frequency")]
public long Frequency { get; set; }
@@ -127,7 +127,7 @@ namespace Streetwriters.Common.Models
// public TaxRatesUsed[] TaxRatesUsed { get; set; }
[JsonPropertyName("totals")]
public Totals Totals { get; set; }
public Totals? Totals { get; set; }
// [JsonPropertyName("adjusted_totals")]
// public AdjustedTotals AdjustedTotals { get; set; }
@@ -139,7 +139,7 @@ namespace Streetwriters.Common.Models
// public AdjustedTotals AdjustedPayoutTotals { get; set; }
[JsonPropertyName("line_items")]
public LineItem[] LineItems { get; set; }
public LineItem[]? LineItems { get; set; }
}
public partial class Totals
@@ -175,7 +175,7 @@ namespace Streetwriters.Common.Models
// public object Earnings { get; set; }
[JsonPropertyName("currency_code")]
public string CurrencyCode { get; set; }
public string? CurrencyCode { get; set; }
}
// public partial class AdjustedTotals
// {
@@ -225,10 +225,10 @@ namespace Streetwriters.Common.Models
public partial class LineItem
{
[JsonPropertyName("id")]
public string Id { get; set; }
public string? Id { get; set; }
[JsonPropertyName("price_id")]
public string PriceId { get; set; }
public string? PriceId { get; set; }
// [JsonPropertyName("quantity")]
// public long Quantity { get; set; }
@@ -247,7 +247,7 @@ namespace Streetwriters.Common.Models
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("proration")]
public Proration Proration { get; set; }
public Proration? Proration { get; set; }
}
// public partial class Product
@@ -322,7 +322,7 @@ namespace Streetwriters.Common.Models
public partial class Proration
{
[JsonPropertyName("billing_period")]
public BillingPeriod BillingPeriod { get; set; }
public BillingPeriod? BillingPeriod { get; set; }
}
// public partial class Totals
@@ -356,20 +356,20 @@ namespace Streetwriters.Common.Models
public partial class Item
{
[JsonPropertyName("price")]
public Price Price { get; set; }
public Price? Price { get; set; }
[JsonPropertyName("quantity")]
public long Quantity { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonPropertyName("proration")]
public Proration Proration { get; set; }
public Proration? Proration { get; set; }
}
public partial class Price
{
[JsonPropertyName("id")]
public string Id { get; set; }
public string? Id { get; set; }
// [JsonPropertyName("description")]
// public string Description { get; set; }
@@ -378,7 +378,7 @@ namespace Streetwriters.Common.Models
// public TypeEnum Type { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
public string? Name { get; set; }
// [JsonPropertyName("product_id")]
// public string ProductId { get; set; }
@@ -500,7 +500,7 @@ namespace Streetwriters.Common.Models
public long PerPage { get; set; }
[JsonPropertyName("next")]
public Uri Next { get; set; }
public Uri? Next { get; set; }
[JsonPropertyName("has_more")]
public bool HasMore { get; set; }
@@ -9,7 +9,7 @@ namespace Streetwriters.Common.Models
public bool Success { get; set; }
[JsonPropertyName("response")]
public PaddleUser[] Users { get; set; }
public PaddleUser[]? Users { get; set; }
}
public class PaddleUser
@@ -24,22 +24,22 @@ namespace Streetwriters.Common.Models
public long UserId { get; set; }
[JsonPropertyName("user_email")]
public string UserEmail { get; set; }
public string? UserEmail { get; set; }
[JsonPropertyName("marketing_consent")]
public bool MarketingConsent { get; set; }
[JsonPropertyName("update_url")]
public string UpdateUrl { get; set; }
public string? UpdateUrl { get; set; }
[JsonPropertyName("cancel_url")]
public string CancelUrl { get; set; }
public string? CancelUrl { get; set; }
[JsonPropertyName("state")]
public string State { get; set; }
public string? State { get; set; }
[JsonPropertyName("signup_date")]
public string SignupDate { get; set; }
public string? SignupDate { get; set; }
[JsonPropertyName("quantity")]
public long Quantity { get; set; }
+2 -2
View File
@@ -22,8 +22,8 @@ namespace Streetwriters.Common.Models
public class MFAConfig
{
public bool IsEnabled { get; set; }
public string PrimaryMethod { get; set; }
public string SecondaryMethod { get; set; }
public required string PrimaryMethod { get; set; }
public string? SecondaryMethod { get; set; }
public int RemainingValidCodes { get; set; }
}
}
+5 -5
View File
@@ -29,25 +29,25 @@ using Streetwriters.Common.Interfaces;
namespace Streetwriters.Common.Models
{
public class Offer : IOffer
public class Offer
{
public Offer()
{
Id = ObjectId.GenerateNewId().ToString();
Id = ObjectId.GenerateNewId();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
[JsonPropertyName("id")]
public string Id { get; set; }
public ObjectId Id { get; set; }
[JsonPropertyName("appId")]
public ApplicationType AppId { get; set; }
[JsonPropertyName("promoCode")]
public string PromoCode { get; set; }
public required string PromoCode { get; set; }
[JsonPropertyName("codes")]
public PromoCode[] Codes { get; set; }
public required PromoCode[] Codes { get; set; }
}
}
@@ -10,7 +10,7 @@ namespace Streetwriters.Common.Models
public partial class PaddleResponse
{
[JsonPropertyName("error")]
public PaddleError Error { get; set; }
public PaddleError? Error { get; set; }
}
public class PaddleError
+1 -1
View File
@@ -35,6 +35,6 @@ namespace Streetwriters.Common.Models
public SubscriptionProvider Provider { get; set; }
[JsonPropertyName("code")]
public string Code { get; set; }
public required string Code { get; set; }
}
}
@@ -9,7 +9,7 @@ namespace Streetwriters.Common.Models
public bool Success { get; set; }
[JsonPropertyName("response")]
public Refund Refund { get; set; }
public required Refund Refund { get; set; }
}
public partial class Refund
+1 -1
View File
@@ -30,6 +30,6 @@ namespace Streetwriters.Common.Models
public bool Success { get; set; }
public int StatusCode { get; set; }
[JsonIgnore]
public HttpContent Content { get; set; }
public HttpContent? Content { get; set; }
}
}
+6 -2
View File
@@ -65,8 +65,12 @@ namespace Streetwriters.Common.Models
[BsonRepresentation(BsonType.Int32)]
[JsonPropertyName("type")]
[Obsolete("Use SubscriptionPlan and SubscriptionStatus instead.")]
public SubscriptionType Type { get; set; }
public SubscriptionType Type
{
get;
[Obsolete("Use SubscriptionPlan and SubscriptionStatus instead.")]
set;
}
[JsonPropertyName("cancelURL")]
public string? CancelURL { get; set; }
@@ -10,40 +10,40 @@ namespace Streetwriters.Common.Models
public partial class SubscriptionPreviewResponse : PaddleResponse
{
[JsonPropertyName("data")]
public SubscriptionPreviewData Data { get; set; }
public SubscriptionPreviewData? Data { get; set; }
}
public partial class SubscriptionPreviewData
{
[JsonPropertyName("currency_code")]
public string CurrencyCode { get; set; }
public string? CurrencyCode { get; set; }
[JsonPropertyName("billing_cycle")]
public BillingCycle BillingCycle { get; set; }
public BillingCycle? BillingCycle { get; set; }
[JsonPropertyName("update_summary")]
public UpdateSummary UpdateSummary { get; set; }
public UpdateSummary? UpdateSummary { get; set; }
[JsonPropertyName("immediate_transaction")]
public TransactionV2 ImmediateTransaction { get; set; }
public TransactionV2? ImmediateTransaction { get; set; }
[JsonPropertyName("next_transaction")]
public TransactionV2 NextTransaction { get; set; }
public TransactionV2? NextTransaction { get; set; }
[JsonPropertyName("recurring_transaction_details")]
public Details RecurringTransactionDetails { get; set; }
public Details? RecurringTransactionDetails { get; set; }
}
public partial class UpdateSummary
{
[JsonPropertyName("charge")]
public UpdateSummaryItem Charge { get; set; }
public UpdateSummaryItem? Charge { get; set; }
[JsonPropertyName("credit")]
public UpdateSummaryItem Credit { get; set; }
public UpdateSummaryItem? Credit { get; set; }
[JsonPropertyName("result")]
public UpdateSummaryItem Result { get; set; }
public UpdateSummaryItem? Result { get; set; }
}
public partial class UpdateSummaryItem
+4 -4
View File
@@ -24,13 +24,13 @@ namespace Streetwriters.Common.Models
public class UserModel
{
[JsonPropertyName("id")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("email")]
public string Email { get; set; }
public required string Email { get; set; }
[JsonPropertyName("phoneNumber")]
public string PhoneNumber { get; set; }
public string? PhoneNumber { get; set; }
[JsonPropertyName("isEmailConfirmed")]
public bool IsEmailConfirmed { get; set; }
@@ -39,7 +39,7 @@ namespace Streetwriters.Common.Models
public bool MarketingConsent { get; set; }
[JsonPropertyName("mfa")]
public MFAConfig MFA { get; set; }
public required MFAConfig MFA { get; set; }
}
}
+9 -9
View File
@@ -30,16 +30,16 @@ namespace Streetwriters.Common
{
public class Server
{
public Server(string originCertPath = null, string originCertKeyPath = null)
public Server(string? originCertPath = null, string? originCertKeyPath = null)
{
if (!string.IsNullOrEmpty(originCertPath) && !string.IsNullOrEmpty(originCertKeyPath))
this.SSLCertificate = X509Certificate2.CreateFromPemFile(originCertPath, originCertKeyPath);
}
public string Id { get; set; }
public string? Id { get; set; }
public int Port { get; set; }
public string Hostname { get; set; }
public Uri PublicURL { get; set; }
public X509Certificate2 SSLCertificate { get; }
public required string Hostname { get; set; }
public Uri? PublicURL { get; set; }
public X509Certificate2? SSLCertificate { get; }
public bool IsSecure { get => this.SSLCertificate != null; }
public override string ToString()
@@ -93,14 +93,14 @@ namespace Streetwriters.Common
public static Server NotesnookAPI { get; } = new(Constants.NOTESNOOK_CERT_PATH, Constants.NOTESNOOK_CERT_KEY_PATH)
{
Port = Constants.NOTESNOOK_SERVER_PORT,
Hostname = Constants.NOTESNOOK_SERVER_HOST,
Hostname = Constants.NOTESNOOK_SERVER_HOST ?? "localhost",
Id = "notesnook-sync"
};
public static Server MessengerServer { get; } = new(Constants.SSE_CERT_PATH, Constants.SSE_CERT_KEY_PATH)
{
Port = Constants.SSE_SERVER_PORT,
Hostname = Constants.SSE_SERVER_HOST,
Hostname = Constants.SSE_SERVER_HOST ?? "localhost",
Id = "sse"
};
@@ -108,14 +108,14 @@ namespace Streetwriters.Common
{
PublicURL = Constants.IDENTITY_SERVER_URL,
Port = Constants.IDENTITY_SERVER_PORT,
Hostname = Constants.IDENTITY_SERVER_HOST,
Hostname = Constants.IDENTITY_SERVER_HOST ?? "localhost",
Id = "auth"
};
public static Server SubscriptionServer { get; } = new(Constants.SUBSCRIPTIONS_CERT_PATH, Constants.SUBSCRIPTIONS_CERT_KEY_PATH)
{
Port = Constants.SUBSCRIPTIONS_SERVER_PORT,
Hostname = Constants.SUBSCRIPTIONS_SERVER_HOST,
Hostname = Constants.SUBSCRIPTIONS_SERVER_HOST ?? "localhost",
Id = "subscription"
};
}
+4 -4
View File
@@ -28,8 +28,8 @@ namespace Streetwriters.Common.Services
string email,
EmailTemplate template,
IClient client,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
GnuPGContext? gpgContext = null,
Dictionary<string, byte[]>? attachments = null
)
{
if (!mailClient.IsConnected)
@@ -78,8 +78,8 @@ namespace Streetwriters.Common.Services
EmailTemplate template,
IClient client,
MailboxAddress sender,
GnuPGContext gpgContext = null,
Dictionary<string, byte[]> attachments = null
GnuPGContext? gpgContext = null,
Dictionary<string, byte[]>? attachments = null
)
{
var builder = new BodyBuilder();
@@ -129,7 +129,7 @@ namespace Streetwriters.Common.Services
public async Task<GetCustomerResponse?> FindCustomerFromTransactionAsync(string transactionId)
{
var transaction = await GetTransactionAsync(transactionId);
if (transaction == null) return null;
if (transaction?.Transaction?.CustomerId == null) return null;
var url = $"{PADDLE_BASE_URI}/customers/{transaction.Transaction.CustomerId}";
var response = await httpClient.GetFromJsonAsync<GetCustomerResponse>(url);
return response;
@@ -18,7 +18,7 @@ namespace Streetwriters.Common.Services
HttpClient httpClient = new HttpClient();
public async Task<ListUsersResponse> ListUsersAsync(
public async Task<ListUsersResponse?> ListUsersAsync(
string subscriptionId,
int results
)
@@ -41,7 +41,7 @@ namespace Streetwriters.Common.Services
return await response.Content.ReadFromJsonAsync<ListUsersResponse>();
}
public async Task<ListPaymentsResponse> ListPaymentsAsync(
public async Task<ListPaymentsResponse?> ListPaymentsAsync(
string subscriptionId,
long planId
)
@@ -66,7 +66,7 @@ namespace Streetwriters.Common.Services
return await response.Content.ReadFromJsonAsync<ListPaymentsResponse>();
}
public async Task<ListTransactionsResponse> ListTransactionsAsync(
public async Task<ListTransactionsResponse?> ListTransactionsAsync(
string subscriptionId
)
{
@@ -86,7 +86,7 @@ namespace Streetwriters.Common.Services
return await response.Content.ReadFromJsonAsync<ListTransactionsResponse>();
}
public async Task<PaddleTransactionUser> FindUserFromOrderAsync(string orderId)
public async Task<PaddleTransactionUser?> FindUserFromOrderAsync(string orderId)
{
var url = $"{PADDLE_BASE_URI}/2.0/order/{orderId}/transactions";
var httpClient = new HttpClient();
@@ -101,7 +101,7 @@ namespace Streetwriters.Common.Services
)
);
var transactions = await response.Content.ReadFromJsonAsync<ListTransactionsResponse>();
if (transactions.Transactions.Length == 0) return null;
if (transactions?.Transactions == null || transactions.Transactions.Length == 0) return null;
return transactions.Transactions[0].User;
}
@@ -123,7 +123,7 @@ namespace Streetwriters.Common.Services
);
var refundResponse = await response.Content.ReadFromJsonAsync<RefundPaymentResponse>();
return refundResponse.Success;
return refundResponse?.Success ?? false;
}
public async Task<bool> CancelSubscriptionAsync(string subscriptionId)
+4 -4
View File
@@ -32,14 +32,14 @@ namespace Streetwriters.Common
{
private readonly ConcurrentDictionary<string, IWampRealmProxy> Channels = new();
public string Endpoint { get; set; }
public string Address { get; set; }
public required string Endpoint { get; set; }
public required string Address { get; set; }
public T Topics { get; set; } = new T();
public string Realm { get; set; }
public required string Realm { get; set; }
private async Task<IWampRealmProxy> GetChannelAsync(string topic)
{
if (!Channels.TryGetValue(topic, out IWampRealmProxy channel) || !channel.Monitor.IsConnected)
if (!Channels.TryGetValue(topic, out IWampRealmProxy? channel) || channel == null || !channel.Monitor.IsConnected)
{
channel = await WampHelper.OpenWampChannelAsync(Address, Realm);
Channels.AddOrUpdate(topic, (key) => channel, (key, old) => channel);
+2 -2
View File
@@ -23,7 +23,7 @@ namespace Streetwriters.Data
{
public class DbSettings : IDbSettings
{
public string DatabaseName { get; set; }
public string ConnectionString { get; set; }
public required string DatabaseName { get; set; }
public required string ConnectionString { get; set; }
}
}
@@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -17,6 +17,7 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
@@ -24,9 +25,6 @@ using System.Security.Claims;
using System.Text.Json;
using System.Threading.Tasks;
using AspNetCore.Identity.Mongo.Model;
using IdentityServer4;
using IdentityServer4.Configuration;
using IdentityServer4.Models;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
@@ -74,7 +72,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(clientId);
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.FindByIdAsync(userId);
var user = await UserManager.FindByIdAsync(userId) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, clientId)) return BadRequest($"Unable to find user with ID '{userId}'.");
switch (type)
@@ -86,16 +84,13 @@ namespace Streetwriters.Identity.Controllers
var result = await UserManager.ConfirmEmailAsync(user, code);
if (!result.Succeeded) return BadRequest(result.Errors.ToErrors());
if (await UserManager.IsInRoleAsync(user, client.Id))
if (await UserManager.IsInRoleAsync(user, client.Id) && client.OnEmailConfirmed != null)
{
await client.OnEmailConfirmed(userId);
}
if (!await UserManager.GetTwoFactorEnabledAsync(user))
{
await MFAService.EnableMFAAsync(user, MFAMethods.Email);
user = await UserManager.GetUserAsync(User);
}
var redirectUrl = $"{client.EmailConfirmedRedirectURL}?userId={userId}";
return RedirectPermanent(redirectUrl);
@@ -122,11 +117,12 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
if (string.IsNullOrEmpty(newEmail))
{
ArgumentNullException.ThrowIfNull(user.Email);
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
@@ -144,7 +140,7 @@ namespace Streetwriters.Identity.Controllers
{
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
return Ok(UserAccountService.GetUserAsync(client.Id, user.Id.ToString()));
}
@@ -156,7 +152,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(form.ClientId);
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.FindByEmailAsync(form.Email);
var user = await UserManager.FindByEmailAsync(form.Email) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId)) return Ok();
var code = await UserManager.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "ResetPassword");
@@ -176,7 +172,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
var subjectId = User.FindFirstValue("sub");
@@ -187,7 +183,7 @@ namespace Streetwriters.Identity.Controllers
ClientId = client.Id,
SubjectId = subjectId
});
grants = grants.Where((grant) => grant.Data.Contains(jti));
grants = jti == null ? [] : grants.Where((grant) => grant.Data.Contains(jti));
if (grants.Any())
{
foreach (var grant in grants)
@@ -203,7 +199,7 @@ namespace Streetwriters.Identity.Controllers
public async Task<IActionResult> GetAccessTokenFromCode([FromForm] GetAccessTokenForm form)
{
if (!Clients.IsValidClient(form.ClientId)) return BadRequest("Invalid clientId.");
var user = await UserManager.FindByIdAsync(form.UserId);
var user = await UserManager.FindByIdAsync(form.UserId) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, form.ClientId))
return BadRequest($"Unable to find user with ID '{form.UserId}'.");
@@ -224,7 +220,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id))
return BadRequest($"Unable to find user with ID '{UserManager.GetUserId(User)}'.");
@@ -232,6 +228,9 @@ namespace Streetwriters.Identity.Controllers
{
case "change_email":
{
ArgumentNullException.ThrowIfNull(form.NewEmail);
ArgumentNullException.ThrowIfNull(form.Password);
ArgumentNullException.ThrowIfNull(form.VerificationCode);
var result = await UserManager.ChangeEmailAsync(user, form.NewEmail, form.VerificationCode);
if (result.Succeeded)
{
@@ -251,6 +250,8 @@ namespace Streetwriters.Identity.Controllers
}
case "change_password":
{
ArgumentNullException.ThrowIfNull(form.OldPassword);
ArgumentNullException.ThrowIfNull(form.NewPassword);
var result = await UserManager.ChangePasswordAsync(user, form.OldPassword, form.NewPassword);
if (result.Succeeded)
{
@@ -261,6 +262,7 @@ namespace Streetwriters.Identity.Controllers
}
case "reset_password":
{
ArgumentNullException.ThrowIfNull(form.NewPassword);
var result = await UserManager.RemovePasswordAsync(user);
if (result.Succeeded)
{
@@ -295,7 +297,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
if (!await UserService.IsUserValidAsync(UserManager, user, client.Id)) return BadRequest($"Unable to find user with ID '{user.Id}'.");
var jti = User.FindFirstValue("jti");
@@ -51,7 +51,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
try
{
@@ -83,7 +83,7 @@ namespace Streetwriters.Identity.Controllers
[HttpGet("codes")]
public async Task<IActionResult> GetRecoveryCodes()
{
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
if (!await UserManager.GetTwoFactorEnabledAsync(user)) return BadRequest("Please enable 2FA.");
return Ok(await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 16));
}
@@ -97,7 +97,7 @@ namespace Streetwriters.Identity.Controllers
var client = Clients.FindClientById(User.FindFirstValue("client_id"));
if (client == null) return BadRequest("Invalid client_id.");
var user = await UserManager.FindByIdAsync(User.FindFirstValue("sub"));
var user = await UserManager.GetUserAsync(User);
if (user == null) return Ok(); // We cannot expose that the user doesn't exist.
await MFAService.SendOTPAsync(user, client, new MultiFactorSetupForm
@@ -111,7 +111,7 @@ namespace Streetwriters.Identity.Controllers
[HttpPatch]
public async Task<IActionResult> EnableAuthenticator([FromForm] MultiFactorEnableForm form)
{
var user = await UserManager.GetUserAsync(User);
var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found.");
if (!await MFAService.VerifyOTPAsync(user, form.VerificationCode, form.Type))
return BadRequest("Invalid verification code.");
@@ -88,6 +88,7 @@ namespace Streetwriters.Identity.Controllers
if (result.Errors.Any((e) => e.Code == "DuplicateEmail"))
{
var user = await UserManager.FindByEmailAsync(form.Email);
if (user == null) return BadRequest(new string[] { "User not found." });
if (!await UserManager.IsInRoleAsync(user, client.Id))
{
@@ -114,6 +115,8 @@ namespace Streetwriters.Identity.Controllers
if (result.Succeeded)
{
var user = await UserManager.FindByEmailAsync(form.Email);
if (user == null) return BadRequest(new string[] { "User not found after creation." });
await UserManager.AddToRoleAsync(user, client.Id);
if (Constants.IS_SELF_HOSTED)
{
@@ -124,7 +127,10 @@ namespace Streetwriters.Identity.Controllers
await UserManager.AddClaimAsync(user, new Claim("platform", PlatformFromUserAgent(base.HttpContext.Request.Headers.UserAgent)));
var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.TokenLink(user.Id.ToString(), code, client.Id, TokenType.CONFRIM_EMAIL);
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
if (!string.IsNullOrEmpty(user.Email) && callbackUrl != null)
{
await EmailSender.SendConfirmationEmailAsync(user.Email, callbackUrl, client);
}
}
return Ok(new
{
@@ -141,8 +147,9 @@ namespace Streetwriters.Identity.Controllers
}
}
string PlatformFromUserAgent(string userAgent)
static string PlatformFromUserAgent(string? userAgent)
{
if (string.IsNullOrEmpty(userAgent)) return "unknown";
return userAgent.Contains("okhttp/") ? "android" : userAgent.Contains("Darwin/") || userAgent.Contains("CFNetwork/") ? "ios" : "web";
}
}
@@ -33,14 +33,14 @@ namespace Microsoft.AspNetCore.Http
/// <param name="context">Http context</param>
/// <param name="allowForwarded">Whether to allow x-forwarded-for header check</param>
/// <returns>IPAddress</returns>
public static IPAddress GetRemoteIPAddress(this HttpContext context, bool allowForwarded = true)
public static IPAddress? GetRemoteIPAddress(this HttpContext context, bool allowForwarded = true)
{
if (allowForwarded)
{
// if you are allowing these forward headers, please ensure you are restricting context.Connection.RemoteIpAddress
// to cloud flare ips: https://www.cloudflare.com/ips/
string header = (context.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? context.Request.Headers["X-Forwarded-For"].FirstOrDefault());
if (IPAddress.TryParse(header, out IPAddress ip))
string? header = context.Request.Headers["CF-Connecting-IP"].FirstOrDefault() ?? context.Request.Headers["X-Forwarded-For"].FirstOrDefault();
if (IPAddress.TryParse(header, out IPAddress? ip))
{
return ip;
}
@@ -48,12 +48,12 @@ namespace Microsoft.AspNetCore.Http
return context.Connection.RemoteIpAddress;
}
static UserAgentService userAgentService = new UserAgentService();
static readonly UserAgentService userAgentService = new();
public static string GetClientInfo(this HttpContext httpContext)
{
var clientIp = httpContext.GetRemoteIPAddress().ToString();
var clientIp = httpContext.GetRemoteIPAddress()?.ToString();
var country = httpContext.Request.Headers["CF-IPCountry"];
var userAgent = httpContext.Request.Headers["User-Agent"];
var userAgent = httpContext.Request.Headers.UserAgent;
var builder = new StringBuilder();
builder.AppendLine($"Date: {DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss")}");
@@ -55,10 +55,9 @@ namespace Microsoft.AspNetCore.Authentication
return Task.FromResult(true);
}
Task<AuthenticationTicket> ITicketStore.RetrieveAsync(string key)
Task<AuthenticationTicket?> ITicketStore.RetrieveAsync(string key)
{
AuthenticationTicket ticket;
_cache.TryGetValue(key, out ticket);
_cache.TryGetValue(key, out AuthenticationTicket? ticket);
return Task.FromResult(ticket);
}
@@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc
{
public static class UrlHelperExtensions
{
public static string TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type)
public static string? TokenLink(this IUrlHelper urlHelper, string userId, string code, string clientId, TokenType type)
{
return urlHelper.ActionLink(
@@ -24,8 +24,10 @@ namespace Microsoft.AspNetCore.Identity
{
public static class UserManagerExtensions
{
public static async Task<User> FindRegisteredUserAsync(this UserManager<User> userManager, string email, string clientId)
public static async Task<User?> FindRegisteredUserAsync(this UserManager<User> userManager, string? email, string clientId)
{
if (email == null) return null;
var user = await userManager.FindByEmailAsync(email);
return user != null && await userManager.IsInRoleAsync(user, clientId) ? user : null;
}
@@ -31,7 +31,7 @@ namespace Streetwriters.Identity.Interfaces
Task<bool> ResetMFAAsync(User user);
Task SetSecondaryMethodAsync(User user, string secondaryMethod);
string GetPrimaryMethod(User user);
string GetSecondaryMethod(User user);
string? GetSecondaryMethod(User user);
Task<int> GetRemainingValidCodesAsync(User user);
bool IsValidMFAMethod(string method);
bool IsValidMFAMethod(string method, User user);
@@ -36,7 +36,7 @@ namespace Streetwriters.Identity.MessageHandlers
var client = Clients.FindClientByAppId(subscription.AppId);
if (client == null || user == null) return;
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id));
IdentityUserClaim<string>? statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == UserService.GetClaimKey(client.Id));
Claim subscriptionClaim = UserService.SubscriptionPlanToClaim(client.Id, subscription);
if (statusClaim?.ClaimValue == subscriptionClaim.Value) return;
if (statusClaim != null)
@@ -34,7 +34,7 @@ namespace Streetwriters.Identity.MessageHandlers
var client = Clients.FindClientByAppId(message.AppId);
if (client == null || user == null) return;
IdentityUserClaim<string> statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
IdentityUserClaim<string>? statusClaim = user.Claims.FirstOrDefault((c) => c.ClaimType == $"{client.Id}:status");
if (statusClaim != null)
{
await userManager.RemoveClaimAsync(user, statusClaim.ToClaim());
@@ -21,12 +21,12 @@ namespace Streetwriters.Identity.Models
{
public class AuthenticatorDetails
{
public string SharedKey
public required string SharedKey
{
get; set;
}
public string AuthenticatorUri
public required string AuthenticatorUri
{
get; set;
}
@@ -31,7 +31,7 @@ namespace Streetwriters.Identity.Models
[Required]
[BindProperty(Name = "email")]
[EmailAddress]
public string NewEmail
public required string NewEmail
{
get; set;
}
@@ -27,21 +27,21 @@ namespace Streetwriters.Identity.Models
{
[Required]
[BindProperty(Name = "authorization_code")]
public string Code
public required string Code
{
get; set;
}
[Required]
[BindProperty(Name = "user_id")]
public string UserId
public required string UserId
{
get; set;
}
[Required]
[BindProperty(Name = "client_id")]
public string ClientId
public required string ClientId
{
get; set;
}
@@ -24,6 +24,6 @@ namespace Streetwriters.Identity.Models
public class MFAPasswordRequiredResponse
{
[JsonPropertyName("token")]
public string Token { get; set; }
public required string Token { get; set; }
}
}
@@ -24,12 +24,12 @@ namespace Streetwriters.Identity.Models
public class MFARequiredResponse
{
[JsonPropertyName("primaryMethod")]
public string PrimaryMethod { get; set; }
public required string PrimaryMethod { get; set; }
[JsonPropertyName("secondaryMethod")]
public string SecondaryMethod { get; set; }
public string? SecondaryMethod { get; set; }
[JsonPropertyName("token")]
public string Token { get; set; }
public string? Token { get; set; }
[JsonPropertyName("phoneNumber")]
public string PhoneNumber { get; set; }
public string? PhoneNumber { get; set; }
}
}
@@ -28,14 +28,14 @@ namespace Streetwriters.Identity.Models
[DataType(DataType.Text)]
[Display(Name = "Authenticator type")]
[BindProperty(Name = "type")]
public string Type { get; set; }
public required string Type { get; set; }
[Required]
[StringLength(6, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
[DataType(DataType.Text)]
[Display(Name = "Verification Code")]
[BindProperty(Name = "code")]
public string VerificationCode { get; set; }
public required string VerificationCode { get; set; }
[BindProperty(Name = "isFallback")]
public bool IsFallback { get; set; }
@@ -28,10 +28,10 @@ namespace Streetwriters.Identity.Models
[Required]
[Display(Name = "Authenticator type")]
[BindProperty(Name = "type")]
public string Type { get; set; }
public required string Type { get; set; }
[Display(Name = "Phone number")]
[BindProperty(Name = "phoneNumber")]
public string PhoneNumber { get; set; }
public string? PhoneNumber { get; set; }
}
}
@@ -27,14 +27,14 @@ namespace Streetwriters.Identity.Models
{
[Required]
[BindProperty(Name = "email")]
public string Email
public required string Email
{
get; set;
}
[Required]
[BindProperty(Name = "client_id")]
public string ClientId
public required string ClientId
{
get; set;
}
+4 -4
View File
@@ -28,7 +28,7 @@ namespace Streetwriters.Identity.Models
[Required]
[StringLength(120, ErrorMessage = "Password must be longer than or equal to 8 characters.", MinimumLength = 8)]
[BindProperty(Name = "password")]
public string Password
public required string Password
{
get; set;
}
@@ -36,20 +36,20 @@ namespace Streetwriters.Identity.Models
[Required]
[BindProperty(Name = "email")]
[EmailAddress]
public string Email
public required string Email
{
get; set;
}
[BindProperty(Name = "username")]
public string Username
public string? Username
{
get; set;
}
[Required]
[BindProperty(Name = "client_id")]
public string ClientId
public required string ClientId
{
get; set;
}
@@ -30,7 +30,7 @@ namespace Streetwriters.Identity.Models
[DataType(DataType.Text)]
[Display(Name = "Authenticator code")]
[BindProperty(Name = "code")]
public string Code { get; set; }
public required string Code { get; set; }
[BindProperty(Name = "rememberMachine")]
public bool RememberMachine { get; set; }
@@ -27,7 +27,7 @@ namespace Streetwriters.Identity.Models
{
[Required]
[BindProperty(Name = "type")]
public string Type
public required string Type
{
get; set;
}
@@ -39,33 +39,33 @@ namespace Streetwriters.Identity.Models
}
[BindProperty(Name = "old_password")]
public string OldPassword
public string? OldPassword
{
get; set;
}
[BindProperty(Name = "new_password")]
public string NewPassword
public string? NewPassword
{
get; set;
}
[BindProperty(Name = "password")]
public string Password
public string? Password
{
get; set;
}
[BindProperty(Name = "new_email")]
public string NewEmail
public string? NewEmail
{
get; set;
}
[BindProperty(Name = "verification_code")]
public string VerificationCode
public string? VerificationCode
{
get; set;
}
+1 -1
View File
@@ -57,7 +57,7 @@ namespace Streetwriters.Identity
{
options.Limits.MaxRequestBodySize = long.MaxValue;
options.ListenAnyIP(Servers.IdentityServer.Port);
if (Servers.IdentityServer.IsSecure)
if (Servers.IdentityServer.IsSecure && Servers.IdentityServer.SSLCertificate != null)
{
options.ListenAnyIP(443, listenerOptions =>
{
@@ -43,9 +43,9 @@ namespace Streetwriters.Identity.Services
{
var result = await base.ProcessAsync(validationResult);
if (result.TryGetValue("sub", out object userId))
if (result.TryGetValue("sub", out object? userId) && userId != null)
{
var user = await UserManager.FindByIdAsync(userId.ToString());
var user = await UserManager.FindByIdAsync(userId.ToString() ?? "");
if (user == null || user.Claims == null) return result;
var verifiedClaim = user.Claims.Find((c) => c.ClaimType == "verified");
@@ -57,7 +57,7 @@ namespace Streetwriters.Identity.Services
user.Claims.ForEach((claim) =>
{
if (claim.ClaimType == "verified" || claim.ClaimType == "hcli") return;
if (claim.ClaimType == null || claim.ClaimType == "verified" || claim.ClaimType == "hcli") return;
result.TryAdd(claim.ClaimType, claim.ClaimValue);
});
result.TryAdd("verified", user.EmailConfirmed.ToString().ToLowerInvariant());
+13 -8
View File
@@ -101,18 +101,18 @@ namespace Streetwriters.Identity.Services
public string GetPrimaryMethod(User user)
{
return this.GetClaimValue(user, MFAService.PRIMARY_METHOD_CLAIM, MFAMethods.Email);
return GetClaimValue(user, MFAService.PRIMARY_METHOD_CLAIM) ?? MFAMethods.Email;
}
public string GetSecondaryMethod(User user)
public string? GetSecondaryMethod(User user)
{
return this.GetClaimValue(user, MFAService.SECONDARY_METHOD_CLAIM);
return GetClaimValue(user, MFAService.SECONDARY_METHOD_CLAIM);
}
public string GetClaimValue(User user, string claimType, string defaultValue = null)
public static string? GetClaimValue(User user, string claimType)
{
var claim = user.Claims.FirstOrDefault((c) => c.ClaimType == claimType);
return claim != null ? claim.ClaimValue : defaultValue;
return claim?.ClaimValue;
}
public Task<int> GetRemainingValidCodesAsync(User user)
@@ -158,6 +158,8 @@ namespace Streetwriters.Identity.Services
await UserManager.ResetAuthenticatorKeyAsync(user);
unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user);
}
ArgumentNullException.ThrowIfNull(unformattedKey);
ArgumentNullException.ThrowIfNull(user.Email);
return new AuthenticatorDetails
{
@@ -183,10 +185,12 @@ namespace Streetwriters.Identity.Services
switch (method)
{
case "email":
ArgumentNullException.ThrowIfNull(user.Email);
string emailOTP = await UserManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);
await EmailSender.Send2FACodeEmailAsync(user.Email, emailOTP, client);
break;
case "sms":
ArgumentNullException.ThrowIfNull(form.PhoneNumber);
await UserManager.SetPhoneNumberAsync(user, form.PhoneNumber);
var id = await SMSSender.SendOTPAsync(form.PhoneNumber, client);
logger.LogInformation("SMS OTP sent for user: {UserId}, SMS ID: {SmsId}", user.Id, id);
@@ -200,13 +204,14 @@ namespace Streetwriters.Identity.Services
{
if (method == MFAMethods.SMS)
{
var id = this.GetClaimValue(user, MFAService.SMS_ID_CLAIM);
var id = GetClaimValue(user, MFAService.SMS_ID_CLAIM);
if (string.IsNullOrEmpty(id)) throw new Exception("Could not find associated SMS verify id. Please try sending the code again.");
if (await SMSSender.VerifyOTPAsync(id, code))
{
// Auto confirm user phone number if not confirmed
if (!await UserManager.IsPhoneNumberConfirmedAsync(user))
{
ArgumentNullException.ThrowIfNull(user.PhoneNumber);
var token = await UserManager.GenerateChangePhoneNumberTokenAsync(user, user.PhoneNumber);
await UserManager.VerifyChangePhoneNumberTokenAsync(user, token, user.PhoneNumber);
}
@@ -238,7 +243,7 @@ namespace Streetwriters.Identity.Services
return method == MFAMethods.Email || method == MFAMethods.SMS ? TokenOptions.DefaultPhoneProvider : UserManager.Options.Tokens.AuthenticatorTokenProvider;
}
private string FormatKey(string unformattedKey)
private static string FormatKey(string unformattedKey)
{
var result = new StringBuilder();
int currentPosition = 0;
@@ -255,7 +260,7 @@ namespace Streetwriters.Identity.Services
return result.ToString().ToLowerInvariant();
}
private string GenerateQrCodeUri(string email, string unformattedKey, string issuer)
private static string GenerateQrCodeUri(string email, string unformattedKey, string issuer)
{
const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6";
@@ -43,7 +43,7 @@ namespace Streetwriters.Identity.Services
public async Task GetProfileDataAsync(ProfileDataRequestContext context)
{
User user = await UserManager.GetUserAsync(context.Subject);
User? user = await UserManager.GetUserAsync(context.Subject);
if (user == null) return;
IList<string> roles = await UserManager.GetRolesAsync(user);
@@ -60,35 +60,35 @@ namespace Streetwriters.Identity.Services
EmailSender = emailSender;
}
EmailTemplate Email2FATemplate = new EmailTemplate
readonly EmailTemplate Email2FATemplate = new()
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/Email2FACode.html"),
Text = File.ReadAllText("Templates/Email2FACode.txt"),
Subject = "Your {{app_name}} account 2FA code",
};
EmailTemplate ConfirmEmailTemplate = new EmailTemplate
readonly EmailTemplate ConfirmEmailTemplate = new()
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/ConfirmEmail.html"),
Text = File.ReadAllText("Templates/ConfirmEmail.txt"),
Subject = "Confirm your {{app_name}} account",
};
EmailTemplate ConfirmChangeEmailTemplate = new EmailTemplate
readonly EmailTemplate ConfirmChangeEmailTemplate = new()
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/EmailChangeConfirmation.html"),
Text = File.ReadAllText("Templates/EmailChangeConfirmation.txt"),
Subject = "Change {{app_name}} account email address",
};
EmailTemplate PasswordResetEmailTemplate = new EmailTemplate
readonly EmailTemplate PasswordResetEmailTemplate = new()
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/ResetAccountPassword.html"),
Text = File.ReadAllText("Templates/ResetAccountPassword.txt"),
Subject = "Reset {{app_name}} account password",
};
EmailTemplate FailedLoginAlertTemplate = new EmailTemplate
readonly EmailTemplate FailedLoginAlertTemplate = new()
{
Html = HtmlHelper.ReadMinifiedHtmlFile("Templates/FailedLoginAlert.html"),
Text = File.ReadAllText("Templates/FailedLoginAlert.txt"),
@@ -97,12 +97,12 @@ namespace Streetwriters.Identity.Services
public async Task Send2FACodeEmailAsync(string email, string code, IClient client)
{
var template = new EmailTemplate
var template = new EmailTemplate()
{
Html = Email2FATemplate.Html,
Text = Email2FATemplate.Text,
Subject = Email2FATemplate.Subject,
Data = new { app_name = client.Name, code = code },
Data = new { app_name = client.Name, code },
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
@@ -113,7 +113,7 @@ namespace Streetwriters.Identity.Services
IClient client
)
{
var template = new EmailTemplate
var template = new EmailTemplate()
{
Html = ConfirmEmailTemplate.Html,
Text = ConfirmEmailTemplate.Text,
@@ -129,12 +129,12 @@ namespace Streetwriters.Identity.Services
IClient client
)
{
var template = new EmailTemplate
var template = new EmailTemplate()
{
Html = ConfirmChangeEmailTemplate.Html,
Text = ConfirmChangeEmailTemplate.Text,
Subject = ConfirmChangeEmailTemplate.Subject,
Data = new { app_name = client.Name, code = code },
Data = new { app_name = client.Name, code },
};
await EmailSender.SendEmailAsync(email, template, client, NNGnuPGContext);
}
@@ -145,7 +145,7 @@ namespace Streetwriters.Identity.Services
IClient client
)
{
var template = new EmailTemplate
var template = new EmailTemplate()
{
Html = PasswordResetEmailTemplate.Html,
Text = PasswordResetEmailTemplate.Text,
@@ -157,7 +157,7 @@ namespace Streetwriters.Identity.Services
public async Task SendFailedLoginAlertAsync(string email, string deviceInfo, IClient client)
{
var template = new EmailTemplate
var template = new EmailTemplate()
{
Html = FailedLoginAlertTemplate.Html,
Text = FailedLoginAlertTemplate.Text,
@@ -176,7 +176,7 @@ namespace Streetwriters.Identity.Services
{
IConfiguration PgpKeySettings { get; set; } = pgpKeySettings;
protected override string GetPasswordForKey(PgpSecretKey key)
protected override string? GetPasswordForKey(PgpSecretKey key)
{
return PgpKeySettings[key.KeyId.ToString("X")];
}
@@ -15,7 +15,7 @@ namespace Streetwriters.Identity.Services
public async Task<UserModel?> GetUserAsync(string clientId, string userId)
{
var user = await userManager.FindByIdAsync(userId);
if (!await UserService.IsUserValidAsync(userManager, user, clientId))
if (user == null || !await UserService.IsUserValidAsync(userManager, user, clientId))
return null;
var claims = await userManager.GetClaimsAsync(user);
@@ -25,7 +25,9 @@ namespace Streetwriters.Identity.Services
{
await mfaService.EnableMFAAsync(user, MFAMethods.Email);
user = await userManager.FindByIdAsync(userId);
ArgumentNullException.ThrowIfNull(user);
}
ArgumentNullException.ThrowIfNull(user.Email);
return new UserModel
{
@@ -46,7 +48,7 @@ namespace Streetwriters.Identity.Services
public async Task DeleteUserAsync(string clientId, string userId, string password)
{
var user = await userManager.FindByIdAsync(userId);
if (!await UserService.IsUserValidAsync(userManager, user, clientId)) return;
if (user == null || !await UserService.IsUserValidAsync(userManager, user, clientId)) return;
if (!await userManager.CheckPasswordAsync(user, password)) throw new Exception("Wrong password.");
@@ -32,7 +32,7 @@ namespace Streetwriters.Identity.Services
private static SubscriptionPlan? GetUserSubscriptionPlan(string clientId, User user)
{
var claimKey = GetClaimKey(clientId);
var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey).ClaimValue;
var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey)?.ClaimValue;
switch (status)
{
case "free":
+2 -2
View File
@@ -263,7 +263,7 @@ namespace Streetwriters.Identity
});
}
private void AddOperationalStore(IServiceCollection services, TokenCleanupOptions tokenCleanUpOptions = null)
private static void AddOperationalStore(IServiceCollection services, TokenCleanupOptions? tokenCleanUpOptions = null)
{
BsonClassMap.RegisterClassMap<PersistedGrant>(cm =>
{
@@ -279,7 +279,7 @@ namespace Streetwriters.Identity
{
q.UseMicrosoftDependencyInjectionJobFactory();
if (tokenCleanUpOptions.Enable)
if (tokenCleanUpOptions?.Enable == true)
{
var jobKey = new JobKey("TokenCleanupJob");
q.AddJob<TokenCleanupJob>(opts => opts.WithIdentity(jobKey));

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