feat: add release digest controls
This commit is contained in:
2597
backend/src/Socialize.Api/Migrations/20260508122220_AddUserPreferredLanguage.Designer.cs
generated
Normal file
2597
backend/src/Socialize.Api/Migrations/20260508122220_AddUserPreferredLanguage.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Socialize.Api.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
internal partial class AddUserPreferredLanguage : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "PreferredLanguage",
|
||||||
|
table: "AspNetUsers",
|
||||||
|
type: "character varying(8)",
|
||||||
|
maxLength: 8,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "en");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(migrationBuilder);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "PreferredLanguage",
|
||||||
|
table: "AspNetUsers");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1556,6 +1556,11 @@ namespace Socialize.Api.Migrations
|
|||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("PreferredLanguage")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("character varying(8)");
|
||||||
|
|
||||||
b.Property<string>("RefreshToken")
|
b.Property<string>("RefreshToken")
|
||||||
.HasMaxLength(44)
|
.HasMaxLength(44)
|
||||||
.HasColumnType("character varying(44)");
|
.HasColumnType("character varying(44)");
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ internal class IdentityService(
|
|||||||
Firstname = user.Firstname,
|
Firstname = user.Firstname,
|
||||||
Lastname = user.Lastname,
|
Lastname = user.Lastname,
|
||||||
BirthDate = user.BirthDate,
|
BirthDate = user.BirthDate,
|
||||||
Address = user.Address
|
Address = user.Address,
|
||||||
|
PreferredLanguage = user.PreferredLanguage
|
||||||
};
|
};
|
||||||
|
|
||||||
ret = userModel;
|
ret = userModel;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ internal class User : IdentityUser<Guid>
|
|||||||
[MaxLength(2048)] public string? PortraitUrl { get; set; }
|
[MaxLength(2048)] public string? PortraitUrl { get; set; }
|
||||||
[MaxLength(256)] public string? GoogleId { get; set; }
|
[MaxLength(256)] public string? GoogleId { get; set; }
|
||||||
[MaxLength(256)] public string? FacebookId { get; set; }
|
[MaxLength(256)] public string? FacebookId { get; set; }
|
||||||
|
[MaxLength(8)] public string PreferredLanguage { get; set; } = "en";
|
||||||
[MaxLength(44)] public string? RefreshToken { get; set; }
|
[MaxLength(44)] public string? RefreshToken { get; set; }
|
||||||
public DateTime RefreshTokenExpiryTime { get; set; }
|
public DateTime RefreshTokenExpiryTime { get; set; }
|
||||||
public DateTimeOffset? LastAuthenticatedAt { get; set; }
|
public DateTimeOffset? LastAuthenticatedAt { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Socialize.Api.Infrastructure.Security;
|
||||||
|
using Socialize.Api.Modules.Identity.Data;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.Identity.Handlers;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
internal record ChangePreferredLanguageRequest(string PreferredLanguage);
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
internal class ChangePreferredLanguageValidator : Validator<ChangePreferredLanguageRequest>
|
||||||
|
{
|
||||||
|
public ChangePreferredLanguageValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.PreferredLanguage)
|
||||||
|
.Must(value => value is "en" or "fr")
|
||||||
|
.WithMessage("Preferred language must be en or fr.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
internal class ChangePreferredLanguageHandler(UserManager userManager)
|
||||||
|
: Endpoint<ChangePreferredLanguageRequest>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/users/preferred-language");
|
||||||
|
Options(o => o.WithTags("Users"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(
|
||||||
|
ChangePreferredLanguageRequest request,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
User? user = await userManager.FindByIdAsync(HttpContext.User.GetUserId().ToString());
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
{
|
||||||
|
await SendNotFoundAsync(ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
user.PreferredLanguage = request.PreferredLanguage;
|
||||||
|
|
||||||
|
IdentityResult result = await userManager.UpdateAsync(user);
|
||||||
|
|
||||||
|
if (result.Succeeded)
|
||||||
|
{
|
||||||
|
await SendOkAsync(ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await SendUnauthorizedAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,7 @@ internal class GetCurrentUserQueryHandler(
|
|||||||
Email = userModel.Email,
|
Email = userModel.Email,
|
||||||
BirthDate = userModel.BirthDate,
|
BirthDate = userModel.BirthDate,
|
||||||
Address = userModel.Address,
|
Address = userModel.Address,
|
||||||
|
PreferredLanguage = userModel.PreferredLanguage,
|
||||||
UserRoles = roles
|
UserRoles = roles
|
||||||
},
|
},
|
||||||
ct);
|
ct);
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ internal class UserDto
|
|||||||
public string? PhoneNumber { get; init; }
|
public string? PhoneNumber { get; init; }
|
||||||
public DateTime? BirthDate { get; init; }
|
public DateTime? BirthDate { get; init; }
|
||||||
public string? Address { get; init; }
|
public string? Address { get; init; }
|
||||||
|
public string PreferredLanguage { get; init; } = "en";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ internal class UserModel
|
|||||||
public string? PhoneNumber { get; init; }
|
public string? PhoneNumber { get; init; }
|
||||||
public DateTime? BirthDate { get; init; }
|
public DateTime? BirthDate { get; init; }
|
||||||
public string? Address { get; init; }
|
public string? Address { get; init; }
|
||||||
|
public string PreferredLanguage { get; init; } = "en";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ internal record ReleaseUpdateUnreadSummaryDto(
|
|||||||
int ImportantUnreadCount,
|
int ImportantUnreadCount,
|
||||||
IReadOnlyCollection<ReleaseUpdateDto> Updates);
|
IReadOnlyCollection<ReleaseUpdateDto> Updates);
|
||||||
|
|
||||||
|
internal record ReleaseUpdateDigestSendResultDto(int SentCount);
|
||||||
|
|
||||||
internal static class ReleaseUpdateDtoMapper
|
internal static class ReleaseUpdateDtoMapper
|
||||||
{
|
{
|
||||||
public static ReleaseUpdateDto ToDto(this ReleaseUpdate update, bool isRead)
|
public static ReleaseUpdateDto ToDto(this ReleaseUpdate update, bool isRead)
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using FastEndpoints;
|
||||||
|
using Socialize.Api.Modules.Identity.Contracts;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Contracts;
|
||||||
|
using Socialize.Api.Modules.ReleaseCommunications.Services;
|
||||||
|
|
||||||
|
namespace Socialize.Api.Modules.ReleaseCommunications.Handlers;
|
||||||
|
|
||||||
|
internal class ForceDeveloperReleaseUpdateDigestEmailsHandler(ReleaseUpdateEmailService emailService)
|
||||||
|
: EndpointWithoutRequest<ReleaseUpdateDigestSendResultDto>
|
||||||
|
{
|
||||||
|
public override void Configure()
|
||||||
|
{
|
||||||
|
Post("/api/developer/release-update-email-digests/force");
|
||||||
|
Roles(KnownRoles.Developer);
|
||||||
|
Options(o => o.WithTags("Release Communications"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task HandleAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
int sentCount = await emailService.SendDueDigestEmailsAsync(
|
||||||
|
TimeSpan.Zero,
|
||||||
|
TimeSpan.Zero,
|
||||||
|
force: true,
|
||||||
|
ct: ct);
|
||||||
|
|
||||||
|
await SendOkAsync(new ReleaseUpdateDigestSendResultDto(sentCount), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,8 @@ internal sealed class ReleaseUpdateEmailDigestBackgroundService(
|
|||||||
int sentCount = await emailService.SendDueDigestEmailsAsync(
|
int sentCount = await emailService.SendDueDigestEmailsAsync(
|
||||||
TimeSpan.FromHours(options.Value.InactiveHoursBeforeDigest),
|
TimeSpan.FromHours(options.Value.InactiveHoursBeforeDigest),
|
||||||
TimeSpan.FromHours(options.Value.DigestIntervalHours),
|
TimeSpan.FromHours(options.Value.DigestIntervalHours),
|
||||||
stoppingToken);
|
force: false,
|
||||||
|
ct: stoppingToken);
|
||||||
if (sentCount > 0 && logger.IsEnabled(LogLevel.Information))
|
if (sentCount > 0 && logger.IsEnabled(LogLevel.Information))
|
||||||
{
|
{
|
||||||
logger.LogInformation("Sent {SentCount} release update digest emails.", sentCount);
|
logger.LogInformation("Sent {SentCount} release update digest emails.", sentCount);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ internal class ReleaseUpdateEmailService(
|
|||||||
public async Task<int> SendDueDigestEmailsAsync(
|
public async Task<int> SendDueDigestEmailsAsync(
|
||||||
TimeSpan inactiveThreshold,
|
TimeSpan inactiveThreshold,
|
||||||
TimeSpan sendInterval,
|
TimeSpan sendInterval,
|
||||||
|
bool force,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
DateTimeOffset now = DateTimeOffset.UtcNow;
|
DateTimeOffset now = DateTimeOffset.UtcNow;
|
||||||
@@ -30,7 +31,7 @@ internal class ReleaseUpdateEmailService(
|
|||||||
foreach (User user in ownerUsers)
|
foreach (User user in ownerUsers)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(user.Email) ||
|
if (string.IsNullOrWhiteSpace(user.Email) ||
|
||||||
!ReleaseUpdateEmailRules.IsInactive(user.LastAuthenticatedAt, inactiveBefore))
|
(!force && !ReleaseUpdateEmailRules.IsInactive(user.LastAuthenticatedAt, inactiveBefore)))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -40,7 +41,7 @@ internal class ReleaseUpdateEmailService(
|
|||||||
.OrderByDescending(receipt => receipt.SentAt)
|
.OrderByDescending(receipt => receipt.SentAt)
|
||||||
.Select(receipt => (DateTimeOffset?)receipt.SentAt)
|
.Select(receipt => (DateTimeOffset?)receipt.SentAt)
|
||||||
.FirstOrDefaultAsync(ct);
|
.FirstOrDefaultAsync(ct);
|
||||||
if (!ReleaseUpdateEmailRules.CanSendDigest(lastDigestSentAt, lastSentBefore))
|
if (!force && !ReleaseUpdateEmailRules.CanSendDigest(lastDigestSentAt, lastSentBefore))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -61,8 +62,8 @@ internal class ReleaseUpdateEmailService(
|
|||||||
|
|
||||||
await emailSender.SendEmailAsync(
|
await emailSender.SendEmailAsync(
|
||||||
user.Email,
|
user.Email,
|
||||||
"What's new in Socialize",
|
GetDigestSubject(user.PreferredLanguage),
|
||||||
BuildDigestEmail(unreadUpdates));
|
BuildDigestEmail(unreadUpdates, user.PreferredLanguage));
|
||||||
|
|
||||||
dbContext.ReleaseUpdateEmailDigestReceipts.Add(new ReleaseUpdateEmailDigestReceipt
|
dbContext.ReleaseUpdateEmailDigestReceipts.Add(new ReleaseUpdateEmailDigestReceipt
|
||||||
{
|
{
|
||||||
@@ -86,25 +87,41 @@ internal class ReleaseUpdateEmailService(
|
|||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string BuildDigestEmail(IReadOnlyCollection<ReleaseUpdate> updates)
|
private string BuildDigestEmail(IReadOnlyCollection<ReleaseUpdate> updates, string? preferredLanguage)
|
||||||
{
|
{
|
||||||
|
bool useFrench = IsFrench(preferredLanguage);
|
||||||
string updateUrl = $"{websiteOptions.Value.FrontendBaseUrl.TrimEnd('/')}/app/updates";
|
string updateUrl = $"{websiteOptions.Value.FrontendBaseUrl.TrimEnd('/')}/app/updates";
|
||||||
string listItems = string.Join(
|
string listItems = string.Join(
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
updates.Select(update => $"""
|
updates.Select(update => $"""
|
||||||
<li>
|
<li>
|
||||||
<strong>{HtmlEncode(update.Title)}</strong><br>{HtmlEncode(update.Summary)}<br>
|
<strong>{HtmlEncode(useFrench ? update.TitleFr : update.Title)}</strong><br>
|
||||||
<strong>{HtmlEncode(update.TitleFr)}</strong><br>{HtmlEncode(update.SummaryFr)}
|
{HtmlEncode(useFrench ? update.SummaryFr : update.Summary)}
|
||||||
</li>
|
</li>
|
||||||
"""));
|
"""));
|
||||||
|
|
||||||
|
string heading = useFrench ? "Nouveautes dans Socialize" : "What's new in Socialize";
|
||||||
|
string linkText = useFrench ? "Ouvrir les nouveautes" : "Open What's New";
|
||||||
|
|
||||||
return $"""
|
return $"""
|
||||||
<h1>What's new in Socialize</h1>
|
<h1>{HtmlEncode(heading)}</h1>
|
||||||
<ul>{listItems}</ul>
|
<ul>{listItems}</ul>
|
||||||
<p><a href="{HtmlEncode(updateUrl)}">Open What's New</a></p>
|
<p><a href="{HtmlEncode(updateUrl)}">{HtmlEncode(linkText)}</a></p>
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetDigestSubject(string? preferredLanguage)
|
||||||
|
{
|
||||||
|
return IsFrench(preferredLanguage)
|
||||||
|
? "Nouveautes dans Socialize"
|
||||||
|
: "What's new in Socialize";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFrench(string? preferredLanguage)
|
||||||
|
{
|
||||||
|
return string.Equals(preferredLanguage, "fr", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private static string HtmlEncode(string? value)
|
private static string HtmlEncode(string? value)
|
||||||
{
|
{
|
||||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||||
|
|||||||
112
frontend/src/api/schema.d.ts
vendored
112
frontend/src/api/schema.d.ts
vendored
@@ -132,6 +132,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/developer/release-update-email-digests/force": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["SocializeApiModulesReleaseCommunicationsHandlersForceDeveloperReleaseUpdateDigestEmailsHandler"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/developer/release-updates/{id}": {
|
"/api/developer/release-updates/{id}": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -580,6 +596,22 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/api/users/preferred-language": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
post: operations["SocializeApiModulesIdentityHandlersChangePreferredLanguageHandler"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/api/users/confirm-email-change": {
|
"/api/users/confirm-email-change": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -1493,6 +1525,10 @@ export interface components {
|
|||||||
titleFr: string;
|
titleFr: string;
|
||||||
descriptionFr: string;
|
descriptionFr: string;
|
||||||
};
|
};
|
||||||
|
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDigestSendResultDto: {
|
||||||
|
/** Format: int32 */
|
||||||
|
sentCount?: number;
|
||||||
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto: {
|
SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto: {
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
@@ -1683,6 +1719,9 @@ export interface components {
|
|||||||
/** Format: binary */
|
/** Format: binary */
|
||||||
file: string;
|
file: string;
|
||||||
};
|
};
|
||||||
|
SocializeApiModulesIdentityHandlersChangePreferredLanguageRequest: {
|
||||||
|
preferredLanguage?: string;
|
||||||
|
};
|
||||||
SocializeApiModulesIdentityHandlersConfirmEmailChangeResponse: {
|
SocializeApiModulesIdentityHandlersConfirmEmailChangeResponse: {
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
@@ -1708,6 +1747,7 @@ export interface components {
|
|||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
birthDate?: string | null;
|
birthDate?: string | null;
|
||||||
address?: string | null;
|
address?: string | null;
|
||||||
|
preferredLanguage?: string;
|
||||||
};
|
};
|
||||||
SystemIOStream: components["schemas"]["SystemMarshalByRefObject"] & {
|
SystemIOStream: components["schemas"]["SystemMarshalByRefObject"] & {
|
||||||
canTimeout?: boolean;
|
canTimeout?: boolean;
|
||||||
@@ -2715,6 +2755,40 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
SocializeApiModulesReleaseCommunicationsHandlersForceDeveloperReleaseUpdateDigestEmailsHandler: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description Success */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDigestSendResultDto"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Forbidden */
|
||||||
|
403: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
SocializeApiModulesReleaseCommunicationsHandlersGetDeveloperReleaseUpdateHandler: {
|
SocializeApiModulesReleaseCommunicationsHandlersGetDeveloperReleaseUpdateHandler: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@@ -3789,6 +3863,44 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
SocializeApiModulesIdentityHandlersChangePreferredLanguageHandler: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody: {
|
||||||
|
content: {
|
||||||
|
"application/json": components["schemas"]["SocializeApiModulesIdentityHandlersChangePreferredLanguageRequest"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
responses: {
|
||||||
|
/** @description No Content */
|
||||||
|
204: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
/** @description Bad Request */
|
||||||
|
400: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/problem+json": components["schemas"]["FastEndpointsErrorResponse"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Unauthorized */
|
||||||
|
401: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content?: never;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
SocializeApiModulesIdentityHandlersConfirmEmailChangeHandler: {
|
SocializeApiModulesIdentityHandlersConfirmEmailChangeHandler: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query: {
|
query: {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const isRefreshingCommits = ref(false);
|
const isRefreshingCommits = ref(false);
|
||||||
|
const isForcingDigestEmails = ref(false);
|
||||||
const error = ref(null);
|
const error = ref(null);
|
||||||
|
|
||||||
const unreadCount = computed(() => unreadSummary.value?.unreadCount ?? 0);
|
const unreadCount = computed(() => unreadSummary.value?.unreadCount ?? 0);
|
||||||
@@ -142,6 +143,16 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function forceDigestEmails() {
|
||||||
|
isForcingDigestEmails.value = true;
|
||||||
|
try {
|
||||||
|
const response = await client.post('/api/developer/release-update-email-digests/force');
|
||||||
|
return response.data;
|
||||||
|
} finally {
|
||||||
|
isForcingDigestEmails.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function linkCommit(sha, releaseUpdateId) {
|
async function linkCommit(sha, releaseUpdateId) {
|
||||||
await client.post(`/api/developer/release-commits/${sha}/link`, { releaseUpdateId });
|
await client.post(`/api/developer/release-commits/${sha}/link`, { releaseUpdateId });
|
||||||
await loadCommits();
|
await loadCommits();
|
||||||
@@ -202,6 +213,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
isLoading,
|
isLoading,
|
||||||
isSaving,
|
isSaving,
|
||||||
isRefreshingCommits,
|
isRefreshingCommits,
|
||||||
|
isForcingDigestEmails,
|
||||||
error,
|
error,
|
||||||
loadUserUpdates,
|
loadUserUpdates,
|
||||||
loadUnreadSummary,
|
loadUnreadSummary,
|
||||||
@@ -214,6 +226,7 @@ export const useReleaseCommunicationsStore = defineStore('release-communications
|
|||||||
archiveUpdate,
|
archiveUpdate,
|
||||||
loadCommits,
|
loadCommits,
|
||||||
refreshCommits,
|
refreshCommits,
|
||||||
|
forceDigestEmails,
|
||||||
linkCommit,
|
linkCommit,
|
||||||
linkCommitsToUpdate,
|
linkCommitsToUpdate,
|
||||||
linkFirstReleaseCommits,
|
linkFirstReleaseCommits,
|
||||||
|
|||||||
@@ -397,12 +397,6 @@
|
|||||||
v-else
|
v-else
|
||||||
class="updates-panel"
|
class="updates-panel"
|
||||||
>
|
>
|
||||||
<div class="panel-header">
|
|
||||||
<div>
|
|
||||||
<h2>{{ t('releaseCommunications.developer.pastReleases') }}</h2>
|
|
||||||
<p>{{ t('releaseCommunications.developer.pastReleasesDescription') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
v-for="update in store.developerUpdates"
|
v-for="update in store.developerUpdates"
|
||||||
:key="update.id"
|
:key="update.id"
|
||||||
@@ -747,7 +741,7 @@
|
|||||||
.update-row {
|
.update-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 3px;
|
gap: 8px;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom: 1px solid #e2e8f0;
|
border-bottom: 1px solid #e2e8f0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@@ -3,12 +3,14 @@ import {defineStore} from 'pinia'
|
|||||||
import {useAuthStore} from "@/features/auth/stores/authStore.js";
|
import {useAuthStore} from "@/features/auth/stores/authStore.js";
|
||||||
import {useClient} from "@/plugins/api.js";
|
import {useClient} from "@/plugins/api.js";
|
||||||
import {useSessionStorage} from "@vueuse/core";
|
import {useSessionStorage} from "@vueuse/core";
|
||||||
|
import {useLanguageStore} from "@/stores/languageStore.js";
|
||||||
|
|
||||||
export const useUserProfileStore = defineStore(
|
export const useUserProfileStore = defineStore(
|
||||||
'user-profile',
|
'user-profile',
|
||||||
() => {
|
() => {
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
|
const languageStore = useLanguageStore()
|
||||||
const isUpdating = ref(false)
|
const isUpdating = ref(false)
|
||||||
const isUploadingPortrait = ref(false)
|
const isUploadingPortrait = ref(false)
|
||||||
const isLoadingCalendarFeed = ref(false)
|
const isLoadingCalendarFeed = ref(false)
|
||||||
@@ -72,6 +74,7 @@ export const useUserProfileStore = defineStore(
|
|||||||
const client = useClient()
|
const client = useClient()
|
||||||
const userResponse = await client.get("/api/users/profile");
|
const userResponse = await client.get("/api/users/profile");
|
||||||
value.value = userResponse.data
|
value.value = userResponse.data
|
||||||
|
languageStore.setLocale(userResponse.data?.preferredLanguage ?? 'en')
|
||||||
} catch (fetchError) {
|
} catch (fetchError) {
|
||||||
console.error(fetchError)
|
console.error(fetchError)
|
||||||
}
|
}
|
||||||
@@ -170,6 +173,28 @@ export const useUserProfileStore = defineStore(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function changePreferredLanguage(preferredLanguage) {
|
||||||
|
isUpdating.value = true
|
||||||
|
error.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = useClient()
|
||||||
|
await client.post(
|
||||||
|
`/api/users/preferred-language`,
|
||||||
|
{
|
||||||
|
preferredLanguage: preferredLanguage
|
||||||
|
})
|
||||||
|
value.value.preferredLanguage = preferredLanguage;
|
||||||
|
languageStore.setLocale(preferredLanguage);
|
||||||
|
} catch (updateError) {
|
||||||
|
console.error(updateError)
|
||||||
|
error.value = 'Failed to update profile.'
|
||||||
|
throw updateError
|
||||||
|
} finally {
|
||||||
|
isUpdating.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function changeAddress(address) {
|
async function changeAddress(address) {
|
||||||
try {
|
try {
|
||||||
const client = useClient()
|
const client = useClient()
|
||||||
@@ -278,6 +303,7 @@ export const useUserProfileStore = defineStore(
|
|||||||
changeBirthday,
|
changeBirthday,
|
||||||
changePhone,
|
changePhone,
|
||||||
changeEmail,
|
changeEmail,
|
||||||
|
changePreferredLanguage,
|
||||||
changeAddress,
|
changeAddress,
|
||||||
changePortrait,
|
changePortrait,
|
||||||
fetchCalendarExportFeed,
|
fetchCalendarExportFeed,
|
||||||
|
|||||||
@@ -19,12 +19,17 @@
|
|||||||
lastname: '',
|
lastname: '',
|
||||||
alias: '',
|
alias: '',
|
||||||
email: '',
|
email: '',
|
||||||
|
preferredLanguage: 'en',
|
||||||
});
|
});
|
||||||
|
|
||||||
const email = computed(() => userProfileStore.user?.email || t('userSettings.noEmail'));
|
const email = computed(() => userProfileStore.user?.email || t('userSettings.noEmail'));
|
||||||
const alias = computed(() => userProfileStore.alias);
|
const alias = computed(() => userProfileStore.alias);
|
||||||
const fullname = computed(() => userProfileStore.fullname);
|
const fullname = computed(() => userProfileStore.fullname);
|
||||||
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
|
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
|
||||||
|
const languageOptions = computed(() => [
|
||||||
|
{ title: t('releaseCommunications.english'), value: 'en' },
|
||||||
|
{ title: t('releaseCommunications.french'), value: 'fr' },
|
||||||
|
]);
|
||||||
const calendarFeedUrl = computed(() => {
|
const calendarFeedUrl = computed(() => {
|
||||||
const feedUrl = userProfileStore.calendarExportFeed?.feedUrl;
|
const feedUrl = userProfileStore.calendarExportFeed?.feedUrl;
|
||||||
|
|
||||||
@@ -42,6 +47,7 @@
|
|||||||
form.lastname = user?.lastname ?? '';
|
form.lastname = user?.lastname ?? '';
|
||||||
form.alias = user?.alias ?? '';
|
form.alias = user?.alias ?? '';
|
||||||
form.email = user?.email ?? '';
|
form.email = user?.email ?? '';
|
||||||
|
form.preferredLanguage = user?.preferredLanguage ?? 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitSettings() {
|
async function submitSettings() {
|
||||||
@@ -56,6 +62,7 @@
|
|||||||
const nextLastname = form.lastname.trim();
|
const nextLastname = form.lastname.trim();
|
||||||
const nextAlias = form.alias.trim();
|
const nextAlias = form.alias.trim();
|
||||||
const nextEmail = form.email.trim();
|
const nextEmail = form.email.trim();
|
||||||
|
const nextPreferredLanguage = form.preferredLanguage;
|
||||||
|
|
||||||
settingsError.value = null;
|
settingsError.value = null;
|
||||||
settingsStatus.value = null;
|
settingsStatus.value = null;
|
||||||
@@ -69,6 +76,10 @@
|
|||||||
await userProfileStore.changeAlias(nextAlias || null);
|
await userProfileStore.changeAlias(nextAlias || null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nextPreferredLanguage !== (user.preferredLanguage ?? 'en')) {
|
||||||
|
await userProfileStore.changePreferredLanguage(nextPreferredLanguage);
|
||||||
|
}
|
||||||
|
|
||||||
let emailChangeRequested = false;
|
let emailChangeRequested = false;
|
||||||
if (nextEmail !== (user.email ?? '')) {
|
if (nextEmail !== (user.email ?? '')) {
|
||||||
await userProfileStore.changeEmail(nextEmail);
|
await userProfileStore.changeEmail(nextEmail);
|
||||||
@@ -248,6 +259,15 @@
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
hide-details
|
hide-details
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<v-select
|
||||||
|
v-model="form.preferredLanguage"
|
||||||
|
:items="languageOptions"
|
||||||
|
:label="t('userSettings.preferredLanguage')"
|
||||||
|
:disabled="userProfileStore.isUpdating"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
mdiCalendar,
|
mdiCalendar,
|
||||||
mdiChevronDown,
|
mdiChevronDown,
|
||||||
mdiCogOutline,
|
mdiCogOutline,
|
||||||
|
mdiEmailOutline,
|
||||||
mdiEyeOffOutline,
|
mdiEyeOffOutline,
|
||||||
mdiFlagVariantOutline,
|
mdiFlagVariantOutline,
|
||||||
mdiFormatListBulleted,
|
mdiFormatListBulleted,
|
||||||
@@ -79,6 +80,17 @@
|
|||||||
contentViewActions.value.find(action => action.active) ?? contentViewActions.value[0]
|
contentViewActions.value.find(action => action.active) ?? contentViewActions.value[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function forceReleaseDigestEmails() {
|
||||||
|
if (!window.confirm(t('releaseCommunications.developer.forceDigestConfirm'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await releaseCommunicationsStore.forceDigestEmails();
|
||||||
|
window.alert(t('releaseCommunications.developer.forceDigestResult', {
|
||||||
|
count: result?.sentCount ?? 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const appBarActions = computed(() => {
|
const appBarActions = computed(() => {
|
||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
return [];
|
return [];
|
||||||
@@ -111,7 +123,15 @@
|
|||||||
}];
|
}];
|
||||||
case 'developer-release-notes':
|
case 'developer-release-notes':
|
||||||
return route.query.tab === 'release-notes'
|
return route.query.tab === 'release-notes'
|
||||||
? []
|
? [
|
||||||
|
{
|
||||||
|
key: 'force-release-digest',
|
||||||
|
label: t('releaseCommunications.developer.forceDigest'),
|
||||||
|
icon: mdiEmailOutline,
|
||||||
|
loading: releaseCommunicationsStore.isForcingDigestEmails,
|
||||||
|
handler: forceReleaseDigestEmails,
|
||||||
|
},
|
||||||
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
key: 'refresh-release-commits',
|
key: 'refresh-release-commits',
|
||||||
@@ -257,6 +277,25 @@
|
|||||||
<v-icon :icon="action.icon" />
|
<v-icon :icon="action.icon" />
|
||||||
<span class="label">{{ action.label }}</span>
|
<span class="label">{{ action.label }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-else-if="action.handler"
|
||||||
|
class="menu-item-action"
|
||||||
|
type="button"
|
||||||
|
:disabled="action.loading"
|
||||||
|
@click="action.handler"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="action.loading"
|
||||||
|
indeterminate
|
||||||
|
size="18"
|
||||||
|
width="2"
|
||||||
|
/>
|
||||||
|
<v-icon
|
||||||
|
v-else
|
||||||
|
:icon="action.icon"
|
||||||
|
/>
|
||||||
|
<span class="label">{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
<router-link
|
<router-link
|
||||||
v-else
|
v-else
|
||||||
:to="action.route"
|
:to="action.route"
|
||||||
|
|||||||
@@ -33,10 +33,15 @@
|
|||||||
isUserMenuOpen.value = !isUserMenuOpen.value;
|
isUserMenuOpen.value = !isUserMenuOpen.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLanguage() {
|
async function toggleLanguage() {
|
||||||
const nextLocale = locale.value === 'en' ? 'fr' : 'en';
|
const nextLocale = locale.value === 'en' ? 'fr' : 'en';
|
||||||
languageStore.setLocale(nextLocale);
|
languageStore.setLocale(nextLocale);
|
||||||
locale.value = nextLocale;
|
locale.value = nextLocale;
|
||||||
|
try {
|
||||||
|
await userProfileStore.changePreferredLanguage(nextLocale);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save preferred language:', error);
|
||||||
|
}
|
||||||
isUserMenuOpen.value = false;
|
isUserMenuOpen.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -631,12 +631,13 @@
|
|||||||
"creationDescription": "Draft a release note from selected commits or edit an existing release.",
|
"creationDescription": "Draft a release note from selected commits or edit an existing release.",
|
||||||
"createReleaseNote": "Create Release Note",
|
"createReleaseNote": "Create Release Note",
|
||||||
"createFirstRelease": "Create First Release",
|
"createFirstRelease": "Create First Release",
|
||||||
"pastReleases": "Past releases",
|
|
||||||
"pastReleasesDescription": "Published, archived, and draft release notes.",
|
|
||||||
"noReleaseNotes": "No release notes yet.",
|
"noReleaseNotes": "No release notes yet.",
|
||||||
"newUpdate": "New update",
|
"newUpdate": "New update",
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
|
"forceDigest": "Send daily email",
|
||||||
|
"forceDigestConfirm": "Send the daily release email now to every eligible user with unread updates?",
|
||||||
|
"forceDigestResult": "Daily release email sent to {count} users.",
|
||||||
"linkedCommits": "Linked commits",
|
"linkedCommits": "Linked commits",
|
||||||
"noLinkedCommits": "No commits linked to this update yet."
|
"noLinkedCommits": "No commits linked to this update yet."
|
||||||
},
|
},
|
||||||
@@ -1110,6 +1111,7 @@
|
|||||||
"lastname": "Last name",
|
"lastname": "Last name",
|
||||||
"fullName": "Full name",
|
"fullName": "Full name",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"preferredLanguage": "Preferred language",
|
||||||
"noEmail": "No email set",
|
"noEmail": "No email set",
|
||||||
"cropperTitle": "Update user portrait",
|
"cropperTitle": "Update user portrait",
|
||||||
"savePortrait": "Save portrait",
|
"savePortrait": "Save portrait",
|
||||||
|
|||||||
@@ -631,12 +631,13 @@
|
|||||||
"creationDescription": "Rédigez une note depuis les commits sélectionnés ou modifiez une release existante.",
|
"creationDescription": "Rédigez une note depuis les commits sélectionnés ou modifiez une release existante.",
|
||||||
"createReleaseNote": "Créer une note de release",
|
"createReleaseNote": "Créer une note de release",
|
||||||
"createFirstRelease": "Créer la première release",
|
"createFirstRelease": "Créer la première release",
|
||||||
"pastReleases": "Releases passées",
|
|
||||||
"pastReleasesDescription": "Notes de release publiées, archivées et brouillons.",
|
|
||||||
"noReleaseNotes": "Aucune note de release pour le moment.",
|
"noReleaseNotes": "Aucune note de release pour le moment.",
|
||||||
"newUpdate": "Nouvelle mise à jour",
|
"newUpdate": "Nouvelle mise à jour",
|
||||||
"publish": "Publier",
|
"publish": "Publier",
|
||||||
"archive": "Archiver",
|
"archive": "Archiver",
|
||||||
|
"forceDigest": "Envoyer l'email quotidien",
|
||||||
|
"forceDigestConfirm": "Envoyer maintenant l'email quotidien des releases a chaque utilisateur eligible avec des mises a jour non lues ?",
|
||||||
|
"forceDigestResult": "Email quotidien des releases envoye a {count} utilisateurs.",
|
||||||
"linkedCommits": "Commits liés",
|
"linkedCommits": "Commits liés",
|
||||||
"noLinkedCommits": "Aucun commit lié à cette mise à jour."
|
"noLinkedCommits": "Aucun commit lié à cette mise à jour."
|
||||||
},
|
},
|
||||||
@@ -1110,6 +1111,7 @@
|
|||||||
"lastname": "Nom",
|
"lastname": "Nom",
|
||||||
"fullName": "Nom complet",
|
"fullName": "Nom complet",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
|
"preferredLanguage": "Langue préférée",
|
||||||
"noEmail": "Aucun email défini",
|
"noEmail": "Aucun email défini",
|
||||||
"cropperTitle": "Mettre à jour le portrait utilisateur",
|
"cropperTitle": "Mettre à jour le portrait utilisateur",
|
||||||
"savePortrait": "Enregistrer le portrait",
|
"savePortrait": "Enregistrer le portrait",
|
||||||
|
|||||||
@@ -521,6 +521,40 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/developer/release-update-email-digests/force": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Release Communications",
|
||||||
|
"Api"
|
||||||
|
],
|
||||||
|
"operationId": "SocializeApiModulesReleaseCommunicationsHandlersForceDeveloperReleaseUpdateDigestEmailsHandler",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Success",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDigestSendResultDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Forbidden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWTBearerAuth": [
|
||||||
|
"developer"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/developer/release-updates/{id}": {
|
"/api/developer/release-updates/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -1890,6 +1924,51 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/users/preferred-language": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"Users",
|
||||||
|
"Api"
|
||||||
|
],
|
||||||
|
"operationId": "SocializeApiModulesIdentityHandlersChangePreferredLanguageHandler",
|
||||||
|
"requestBody": {
|
||||||
|
"x-name": "ChangePreferredLanguageRequest",
|
||||||
|
"description": "",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/SocializeApiModulesIdentityHandlersChangePreferredLanguageRequest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": true,
|
||||||
|
"x-position": 1
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "No Content"
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Bad Request",
|
||||||
|
"content": {
|
||||||
|
"application/problem+json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/FastEndpointsErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Unauthorized"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"JWTBearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/users/confirm-email-change": {
|
"/api/users/confirm-email-change": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@@ -5010,6 +5089,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateDigestSendResultDto": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"sentCount": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto": {
|
"SocializeApiModulesReleaseCommunicationsContractsReleaseUpdateUnreadSummaryDto": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -5618,6 +5707,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"SocializeApiModulesIdentityHandlersChangePreferredLanguageRequest": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"preferredLanguage": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"SocializeApiModulesIdentityHandlersConfirmEmailChangeResponse": {
|
"SocializeApiModulesIdentityHandlersConfirmEmailChangeResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -5714,6 +5812,9 @@
|
|||||||
"address": {
|
"address": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"preferredLanguage": {
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7712,4 +7813,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user