Skip to content

Commit

Permalink
Merge pull request #1692 from exceptionless/feature/shadcn-forms
Browse files Browse the repository at this point in the history
Use shadcn forms
  • Loading branch information
niemyjski authored Oct 24, 2024
2 parents e9512dd + c043b9f commit b5f73ef
Show file tree
Hide file tree
Showing 94 changed files with 3,169 additions and 1,914 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ jobs:
- name: Lint Client
run: npm run lint

- name: Check
run: npm run check

- name: Build
run: npm run build

Expand Down
2 changes: 2 additions & 0 deletions src/Exceptionless.Core/Bootstrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
using Exceptionless.Core.Serialization;
using Exceptionless.Core.Services;
using Exceptionless.Core.Utility;
using Exceptionless.Core.Validation;
using Exceptionless.Serializer;
using FluentValidation;
using Foundatio.Caching;
Expand Down Expand Up @@ -145,6 +146,7 @@ public static void RegisterServices(IServiceCollection services, AppOptions appO
services.AddSingleton<PersistentEventQueryValidator>();
services.AddSingleton<StackQueryValidator>();

services.AddSingleton<MiniValidationValidator>();
services.AddSingleton(typeof(IValidator<>), typeof(Bootstrapper).Assembly);
services.AddSingleton(typeof(IPipelineAction<EventContext>), typeof(Bootstrapper).Assembly);
services.AddSingleton(typeof(IPlugin), typeof(Bootstrapper).Assembly);
Expand Down
1 change: 1 addition & 0 deletions src/Exceptionless.Core/Exceptionless.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<PackageReference Include="FluentValidation" Version="11.10.0" />
<PackageReference Include="Foundatio.Extensions.Hosting" Version="$(FoundatioVersion)" />
<PackageReference Include="Foundatio.JsonNet" Version="$(FoundatioVersion)" />
<PackageReference Include="MiniValidation" Version="0.9.1" />
<PackageReference Include="NEST.JsonNetSerializer" Version="7.17.5" />
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="McSherry.SemanticVersioning" Version="1.4.1" />
Expand Down
5 changes: 3 additions & 2 deletions src/Exceptionless.Core/Extensions/IdentityUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ public static ClaimsIdentity ToIdentity(this User user, Token? token = null)
claims.Add(new Claim(ClaimTypes.Role, AuthorizationRoles.User));
}

return new ClaimsIdentity(claims, UserAuthenticationType);
string authenticationType = token is { Type: TokenType.Access } ? TokenAuthenticationType : UserAuthenticationType;
return new ClaimsIdentity(claims, authenticationType);
}

public static bool IsAuthenticated(this ClaimsPrincipal principal)
Expand Down Expand Up @@ -146,7 +147,7 @@ public static string[] GetOrganizationIds(this ClaimsPrincipal principal)
if (String.IsNullOrEmpty(ids))
return [];

return ids.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
return ids.Split([','], StringSplitOptions.RemoveEmptyEntries);
}

public static string? GetProjectId(this ClaimsPrincipal principal)
Expand Down
3 changes: 2 additions & 1 deletion src/Exceptionless.Core/Extensions/UserExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Exceptionless.Core.Models;
using Exceptionless.DateTimeExtensions;

namespace Exceptionless.Core.Extensions;

Expand Down Expand Up @@ -29,7 +30,7 @@ public static void MarkEmailAddressVerified(this User user)

public static bool HasValidVerifyEmailAddressTokenExpiration(this User user, TimeProvider timeProvider)
{
return user.VerifyEmailAddressTokenExpiration != DateTime.MinValue && user.VerifyEmailAddressTokenExpiration >= timeProvider.GetUtcNow().UtcDateTime;
return user.VerifyEmailAddressTokenExpiration.IsAfterOrEqual(timeProvider.GetUtcNow().UtcDateTime);
}

public static void ResetPasswordResetToken(this User user)
Expand Down
3 changes: 2 additions & 1 deletion src/Exceptionless.Core/Jobs/EventPostsJob.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Exceptionless.Core.Repositories;
using Exceptionless.Core.Repositories.Base;
using Exceptionless.Core.Services;
using Exceptionless.Core.Validation;
using FluentValidation;
using Foundatio.Jobs;
using Foundatio.Queues;
Expand Down Expand Up @@ -208,7 +209,7 @@ protected override async Task<JobResult> ProcessQueueEntryAsync(QueueEntryContex
continue;

if (!isInternalProject) _logger.LogError(ctx.Exception, "Error processing EventPost {QueueEntryId} {FilePath}: {Message}", entry.Id, payloadPath, ctx.ErrorMessage);
if (ctx.Exception is ValidationException)
if (ctx.Exception is ValidationException or MiniValidatorException)
continue;

errorCount++;
Expand Down
46 changes: 41 additions & 5 deletions src/Exceptionless.Core/Models/User.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using Foundatio.Repositories.Models;

namespace Exceptionless.Core.Models;

public class User : IIdentity, IHaveDates
public record User : IIdentity, IHaveDates, IValidatableObject
{
/// <summary>
/// Unique id that identifies an user.
Expand All @@ -13,19 +14,22 @@ public class User : IIdentity, IHaveDates
/// <summary>
/// The organizations that the user has access to.
/// </summary>
public ICollection<string> OrganizationIds { get; set; } = new Collection<string>();
public ICollection<string> OrganizationIds { get; } = new Collection<string>();

public string? Password { get; set; }
public string? Salt { get; set; }
public string? PasswordResetToken { get; set; }
public DateTime PasswordResetTokenExpiration { get; set; }
public ICollection<OAuthAccount> OAuthAccounts { get; set; } = new Collection<OAuthAccount>();
public ICollection<OAuthAccount> OAuthAccounts { get; } = new Collection<OAuthAccount>();

/// <summary>
/// Gets or sets the users Full Name.
/// </summary>
[Required]
public string FullName { get; set; } = null!;

[Required]
[EmailAddress]
public string EmailAddress { get; set; } = null!;
public bool EmailNotificationsEnabled { get; set; } = true;
public bool IsEmailAddressVerified { get; set; }
Expand All @@ -35,10 +39,42 @@ public class User : IIdentity, IHaveDates
/// <summary>
/// Gets or sets the users active state.
/// </summary>
public bool IsActive { get; set; } = true;
public bool IsActive { get; init; } = true;

public ICollection<string> Roles { get; set; } = new Collection<string>();
public ICollection<string> Roles { get; init; } = new Collection<string>();

public DateTime CreatedUtc { get; set; }
public DateTime UpdatedUtc { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (IsEmailAddressVerified)
{
if (VerifyEmailAddressToken is not null)
{
yield return new ValidationResult("A verify email address token cannot be set if the email address has been verified.",
[nameof(VerifyEmailAddressToken)]);
}

if (VerifyEmailAddressTokenExpiration != default)
{
yield return new ValidationResult("A verify email address token expiration cannot be set if the email address has been verified.",
[nameof(VerifyEmailAddressTokenExpiration)]);
}
}
else
{
if (String.IsNullOrWhiteSpace(VerifyEmailAddressToken))
{
yield return new ValidationResult("A verify email address token must be set if the email address has not been verified.",
[nameof(VerifyEmailAddressToken)]);
}

if (VerifyEmailAddressTokenExpiration == default)
{
yield return new ValidationResult("A verify email address token expiration must be set if the email address has not been verified.",
[nameof(VerifyEmailAddressTokenExpiration)]);
}
}
}
}
8 changes: 6 additions & 2 deletions src/Exceptionless.Core/Repositories/Base/RepositoryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ namespace Exceptionless.Core.Repositories;

public abstract class RepositoryBase<T> : ElasticRepositoryBase<T> where T : class, IIdentity, new()
{
protected readonly IValidator<T> _validator;
protected readonly IValidator<T>? _validator;
protected readonly AppOptions _options;

public RepositoryBase(IIndex index, IValidator<T> validator, AppOptions options) : base(index)
public RepositoryBase(IIndex index, IValidator<T>? validator, AppOptions options) : base(index)
{
_validator = validator;
_options = options;
Expand All @@ -25,6 +25,10 @@ public RepositoryBase(IIndex index, IValidator<T> validator, AppOptions options)

protected override Task ValidateAndThrowAsync(T document)
{
// TODO: Move this to MiniValidationValidator
if (_validator is null)
return Task.CompletedTask;

return _validator.ValidateAndThrowAsync(document);
}

Expand Down
15 changes: 9 additions & 6 deletions src/Exceptionless.Core/Repositories/ProjectRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,24 @@ private void OnDocumentsChanging(object sender, DocumentsChangeEventArgs<Project
if (String.IsNullOrEmpty(projectId))
return null;

var configCacheValue = await Cache.GetAsync<Project>($"config:{projectId}");
string cacheKey = ConfigCacheKey(projectId);
var configCacheValue = await Cache.GetAsync<Project>(cacheKey);
if (configCacheValue.HasValue)
return configCacheValue.Value;

var project = await FindOneAsync(q => q.Id(projectId).Include(p => p.Configuration, p => p.OrganizationId));
if (project?.Document is null)
return null;

await Cache.AddAsync($"config:{projectId}", project.Document);
await Cache.AddAsync(cacheKey, project.Document);

return project.Document;
}

public Task<CountResult> GetCountByOrganizationIdAsync(string organizationId)
{
ArgumentException.ThrowIfNullOrEmpty(organizationId);

return CountAsync(q => q.Organization(organizationId), o => o.Cache(String.Concat("Organization:", organizationId)));
return CountAsync(q => q.Organization(organizationId), o => o.Cache(OrganizationCacheKey(organizationId)));
}

public Task<FindResults<Project>> GetByOrganizationIdsAsync(ICollection<string> organizationIds, CommandOptionsDescriptor<Project>? options = null)
Expand Down Expand Up @@ -91,11 +91,14 @@ public async Task IncrementNextSummaryEndOfDayTicksAsync(IReadOnlyCollection<Pro
protected override async Task InvalidateCacheAsync(IReadOnlyCollection<ModifiedDocument<Project>> documents, ChangeType? changeType = null)
{
var organizations = documents.Select(d => d.Value.OrganizationId).Distinct().Where(id => !String.IsNullOrEmpty(id));
await Cache.RemoveAllAsync(organizations.Select(id => $"count:Organization:{id}"));
await Cache.RemoveAllAsync(organizations.Select(id => $"count:{OrganizationCacheKey(id)}"));

var configCacheKeys = documents.Select(d => $"config:{d.Value.Id}");
var configCacheKeys = documents.Select(d => ConfigCacheKey(d.Value.Id));
await Cache.RemoveAllAsync(configCacheKeys);

await base.InvalidateCacheAsync(documents, changeType);
}

private static string ConfigCacheKey(string projectId) => String.Concat("config:", projectId);
private static string OrganizationCacheKey(string organizationId) => String.Concat("Organization:", organizationId);
}
15 changes: 12 additions & 3 deletions src/Exceptionless.Core/Repositories/UserRepository.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Exceptionless.Core.Extensions;
using Exceptionless.Core.Repositories.Configuration;
using FluentValidation;
using Exceptionless.Core.Validation;
using Foundatio.Repositories;
using Foundatio.Repositories.Models;
using Foundatio.Repositories.Options;
Expand All @@ -11,13 +11,22 @@ namespace Exceptionless.Core.Repositories;

public class UserRepository : RepositoryBase<User>, IUserRepository
{
public UserRepository(ExceptionlessElasticConfiguration configuration, IValidator<User> validator, AppOptions options)
: base(configuration.Users, validator, options)
private readonly MiniValidationValidator _miniValidationValidator;

public UserRepository(ExceptionlessElasticConfiguration configuration, MiniValidationValidator validator, AppOptions options)
: base(configuration.Users, null, options)
{
_miniValidationValidator = validator;
DefaultConsistency = Consistency.Immediate;
AddPropertyRequiredForRemove(u => u.EmailAddress, u => u.OrganizationIds);
}

protected override Task ValidateAndThrowAsync(User document)
{
// TOOD: Deprecate this once all are converted to MiniValidationValidator.
return _miniValidationValidator.ValidateAndThrowAsync(document);
}

public async Task<User?> GetByEmailAddressAsync(string emailAddress)
{
if (String.IsNullOrWhiteSpace(emailAddress))
Expand Down
3 changes: 1 addition & 2 deletions src/Exceptionless.Core/Services/SlackService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,7 @@ public SlackService(IQueue<WebHookNotification> webHookNotificationQueue, Format

if (!result.ok)
{
_logger.LogWarning("Error getting access token: {Message}, Response: {Response}", result.error ?? result.warning, result);
return null;
throw new Exception($"Error getting access token: {result.error ?? result.warning}, Response: {result}");
}

var token = new SlackToken
Expand Down
40 changes: 40 additions & 0 deletions src/Exceptionless.Core/Validation/MiniValidationValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text;
using MiniValidation;

namespace Exceptionless.Core.Validation;

public class MiniValidationValidator(IServiceProvider serviceProvider)
{
public ValueTask<(bool IsValid, IDictionary<string, string[]> Errors)> ValidateAsync<T>(T instance)
{
return MiniValidator.TryValidateAsync(instance, serviceProvider, recurse: true);
}

public async Task ValidateAndThrowAsync<T>(T instance)
{
(bool isValid, var errors) = await ValidateAsync(instance);
if (isValid)
return;

throw new MiniValidatorException("Please correct the specified errors and try again", errors);
}
}

public class MiniValidatorException(string message, IDictionary<string, string[]> errors) : Exception(message)
{
public IDictionary<string, string[]> Errors { get; } = errors;

public string FormattedErrorMessages()
{
var errorMessages = new StringBuilder();
errorMessages.AppendLine(Message);
foreach (var error in Errors)
{
errorMessages.Append("- ").Append(error.Key).Append(": ");
errorMessages.AppendJoin(", ", error.Value);
errorMessages.AppendLine();
}

return errorMessages.ToString();
}
}
16 changes: 0 additions & 16 deletions src/Exceptionless.Core/Validation/UserValidator.cs

This file was deleted.

5 changes: 4 additions & 1 deletion src/Exceptionless.Web/ClientApp/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"debug.allowBreakpointsEverywhere": true,
"cSpell.words": [
"acacode",
"classvalidator",
"clsx",
"cmdk",
"colour",
Expand Down Expand Up @@ -59,6 +60,8 @@
"workbench.editor.customLabels.patterns": {
"**/lib/**/*.ts": "${dirname}/${filename}.${extname}",
"**/routes/**/+page.svelte": "${dirname(1)}/${dirname}",
"**/routes/**/+layout.svelte": "${dirname}/+layout.svelte"
"**/routes/**/+layout.svelte": "${dirname}/+layout.svelte",
"**/routes/**/routes.ts": "${dirname}/routes.ts",
"**/lib/**/index.ts": "${dirname}/index.ts"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class <%~ contract.name %> {
<% for (const field of contract.$content) { %>
<%~ includeFile('@base/object-field-jsdoc.ejs', { ...it, field }) %>
<%~ includeFile('./object-field-class-validator.ejs', { ...it, field }) %>
<%~ field.name %><%~ field.isRequired ? '!' : '?' %>: <%~ field.value.replaceAll('any', 'unknown') %>;
<%~ field.name %><%~ field.isRequired || field.nullable !== true ? '!' : '' %>: <%~ field.value.replaceAll('any', 'unknown') %><%~ field.type === "object" && field.nullable === true ? ' | null' : '' %><%~ field.nullable === true ? ' = null' : '' %>;
<% } %>
}

Loading

0 comments on commit b5f73ef

Please sign in to comment.