Вот мой концентратор:
using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;
using System.Text.RegularExpressions;
using limbo.dating.Server.DTO.Messages;
using Microsoft.AspNetCore.Authorization;
namespace limbo.dating.Server.Hubs
{
[Authorize]
public class FriendHub : Hub
{
private readonly ILogger _logger;
public FriendHub(ILogger logger)
{
_logger = logger;
}
public async Task JoinUserGroup()
{
var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId != null)
{
await Groups.AddToGroupAsync(Context.ConnectionId, userId);
_logger.LogInformation($"User {userId} joined their notification group.");
}
}
public override Task OnConnectedAsync()
{
Console.WriteLine("SignalR client connected: " + Context.ConnectionId);
return base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId != null)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, userId);
}
await base.OnDisconnectedAsync(exception);
}
//Messaging methods
///
/// Send a message to a specific friend
///
public async Task SendMessageToFriend(CreateMessageDto createMessageDto)
{
//TODO: senderId is null
var senderId = Context.UserIdentifier;
if (string.IsNullOrEmpty(senderId)) return;
await Clients.Group(createMessageDto.RecipientId).SendAsync("ReceiveMessage", new
{
MessageId = createMessageDto.MessageId,
SenderId = senderId,
RecipientId = createMessageDto.RecipientId,
Content = createMessageDto.Content,
Timestamp = DateTime.UtcNow,
IsRead = false
});
// Also send to sender's other devices
await Clients.Group(senderId).SendAsync("MessageSentConfirmation", new
{
MessageId = createMessageDto.MessageId,
RecipientId = createMessageDto.RecipientId,
Timestamp = DateTime.UtcNow
});
}
///
/// Notify when a message has been read
///
public async Task NotifyMessageRead(string messageId, string senderId)
{
await Clients.Group(senderId).SendAsync("MessageRead", new
{
MessageId = messageId,
ReadAt = DateTime.UtcNow
});
}
///
/// Send typing indicator to a friend
///
public async Task SendTypingIndicator(string recipientId, bool isTyping)
{
var senderId = Context.UserIdentifier;
if (string.IsNullOrEmpty(senderId)) return;
await Clients.Group(recipientId).SendAsync("FriendIsTyping", new
{
SenderId = senderId,
IsTyping = isTyping
});
}
///
/// Notify when messages are being loaded (for UI feedback)
///
public async Task NotifyLoadingMessages(string recipientId)
{
await Clients.Group(recipientId).SendAsync("FriendIsLoadingMessages");
}
///
/// Notify when new messages are available in a conversation
///
public async Task NotifyNewMessagesAvailable(string friendId)
{
var userId = Context.UserIdentifier;
await Clients.Group(friendId).SendAsync("NewMessagesAvailable", userId);
}
}
}
Here is my Program.cs:
//https://github.com/MoonriseSoftwareCali ... y.CosmosDb
//https://github.com/MoonriseSoftwareCali ... .cshtml.cs
using AspNetCore.Identity.CosmosDb;
using AspNetCore.Identity.CosmosDb.Containers;
using AspNetCore.Identity.CosmosDb.Extensions;
using limbo.dating.Server;
using limbo.dating.Server.Controllers;
using limbo.dating.Server.Data;
using limbo.dating.Server.Data.AspNetCore.Identity.CosmosDb.Example.Data;
using limbo.dating.Server.Hubs;
using limbo.dating.Server.Models;
using limbo.dating.Server.Models.AngularCoreCosmos.Models;
using limbo.dating.Server.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Azure;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
using limbo.dating.Server.Models.limbo.dating.Server.Models;
var builder = WebApplication.CreateBuilder(args);
// The Cosmos connection string
var connectionString = builder.Configuration.GetConnectionString("ApplicationDbContextConnection");
var cosmosAccountKey = builder.Configuration.GetValue("CosmosAccountKey");
// Name of the Cosmos database to use
var cosmosIdentityDbName = builder.Configuration.GetValue("CosmosIdentityDbName");
// If this is set, the Cosmos identity provider will:
// 1. Create the database if it does not already exist.
// 2. Create the required containers if they do not already exist.
// IMPORTANT: Remove this setting if after first run. It will improve startup performance.
var setupCosmosDb = builder.Configuration.GetValue("SetupCosmosDb");
// If the following is set, it will create the Cosmos database and
// required containers.
if (bool.TryParse(setupCosmosDb, out var setup) && setup)
{
var builder1 = new DbContextOptionsBuilder();
builder1.UseCosmos(connectionString, cosmosIdentityDbName);
await using (var dbContext = new LimboDatingDbContext(builder1.Options))
{
await dbContext.Database.EnsureCreatedAsync();
}
}
builder.Services.AddDbContext(options =>
options.UseCosmos(connectionString: connectionString, databaseName: cosmosIdentityDbName));
builder.Services.AddCosmosIdentity(
options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.User.RequireUniqueEmail = true;
options.Password.RequiredLength = 3;
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Lockout.AllowedForNewUsers = true;
}
// Always a good idea
)
//.AddDefaultUI() // Use this if Identity Scaffolding is in use
.AddRoles() // be able to add roles
.AddRoleManager() // be able to make use of RoleManager
.AddEntityFrameworkStores() // providing our context
.AddSignInManager() // make use of Signin manager
.AddUserManager() // make use of UserManager to create users
.AddEntityFrameworkStores() // providing our context
.AddSignInManager() // make use of Signin manager
.AddUserManager() // make use of UserManager to create users
.AddDefaultTokenProviders();
builder.Services.AddAzureClients(clientBuilder =>
{
clientBuilder.AddBlobServiceClient(builder.Configuration.GetConnectionString("AzureBlobStorage"));
});
builder.Services.AddOptions()
.BindConfiguration(ImagesController.AzureBlobStorageSettings.SectionName)
.ValidateDataAnnotations();
// be able to inject JWTService class inside our Controllers
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
// be able to authenticate users using JWT
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = "JWT_OR_COOKIES"; // Custom policy to check both
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddCookie("Cookies", options =>
{
options.LoginPath = "/Account/Login";
options.AccessDeniedPath = "/Account/AccessDenied";
options.Events.OnRedirectToLogin = context =>
{
// Skip redirect for API calls
if (context.Request.Path.StartsWithSegments("/api"))
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
}
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// validate the token based on the key we have provided inside appsettings.development.json JWT:Key
ValidateIssuerSigningKey = true,
// the issuer singning key based on JWT:Key
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Key"])),
// the issuer which in here is the api project url we are using
ValidIssuer = builder.Configuration["JWT:Issuer"],
// validate the issuer (who ever is issuing the JWT)
ValidateIssuer = true,
// don't validate audience (angular side)
ValidateAudience = false,
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
// Only set the token if the request is for the SignalR hub
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/friendHub"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
})
.AddPolicyScheme("JWT_OR_COOKIES", "JWT_OR_COOKIES", options =>
{
options.ForwardDefaultSelector = context =>
{
string authorization = context.Request.Headers.Authorization;
if (!string.IsNullOrEmpty(authorization) && authorization.StartsWith("Bearer "))
return "Bearer";
return "Cookies";
};
});
builder.Services.AddHttpClient();
builder.Services.AddCors(options =>
{
options.AddPolicy(name: "AllowAll",
builder =>
{
builder.WithOrigins("https://localhost:4200")
.AllowAnyHeader()
//.AllowAnyOrigin()
.AllowAnyMethod()
.AllowCredentials()
.WithExposedHeaders("WWW-Authenticate")
.SetPreflightMaxAge(TimeSpan.FromSeconds(86400));
});
});
builder.Services.Configure(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
var errors = actionContext.ModelState
.Where(x => x.Value.Errors.Count > 0)
.SelectMany(x => x.Value.Errors)
.Select(x => x.ErrorMessage).ToArray();
var toReturn = new
{
Errors = errors
};
return new BadRequestObjectResult(toReturn);
};
});
builder.Services.AddAuthorization(opt =>
{
opt.AddPolicy("AdminPolicy", policy => policy.RequireRole("Admin"));
opt.AddPolicy("ManagerPolicy", policy => policy.RequireRole("Manager"));
opt.AddPolicy("PlayerPolicy", policy => policy.RequireRole("Player"));
opt.AddPolicy("AdminOrManagerPolicy", policy => policy.RequireRole("Admin", "Manager"));
opt.AddPolicy("AdminAndManagerPolicy", policy => policy.RequireRole("Admin").RequireRole("Manager"));
opt.AddPolicy("AllRolePolicy", policy => policy.RequireRole("Admin", "Manager", "Player"));
opt.AddPolicy("AdminEmailPolicy", policy => policy.RequireClaim(ClaimTypes.Email, "admin@example.com"));
opt.AddPolicy("MillerSurnamePolicy", policy => policy.RequireClaim(ClaimTypes.Surname, "miller"));
opt.AddPolicy("ManagerEmailAndWilsonSurnamePolicy", policy => policy.RequireClaim(ClaimTypes.Surname, "wilson")
.RequireClaim(ClaimTypes.Email, "manager@example.com"));
opt.AddPolicy("VIPPolicy", policy => policy.RequireAssertion(context => SD.VIPPolicy(context)));
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// Register the repository with a synchronous factory that blocks (only during startup)
builder.Services.AddSingleton(provider =>
{
var config = provider.GetRequiredService();
// Block synchronously during app startup (acceptable for initialization)
return DocumentDBRepository.CreateAsync(connectionString, cosmosAccountKey, cosmosIdentityDbName)
.GetAwaiter()
.GetResult();
});
builder.Services.AddSingleton(provider =>
{
var config = provider.GetRequiredService();
// Block synchronously during app startup (acceptable for initialization)
return DocumentDBRepository.CreateAsync(connectionString, cosmosAccountKey, cosmosIdentityDbName)
.GetAwaiter()
.GetResult();
});
builder.Services.AddSingleton(provider =>
{
var config = provider.GetRequiredService();
// Block synchronously during app startup (acceptable for initialization)
return DocumentDBRepository.CreateAsync(connectionString, cosmosAccountKey, cosmosIdentityDbName)
.GetAwaiter()
.GetResult();
});
builder.Services.AddSignalR(hubOptions => {
// Timeout settings
hubOptions.ClientTimeoutInterval = TimeSpan.FromMinutes(2);
hubOptions.HandshakeTimeout = TimeSpan.FromSeconds(15);
// Keep-alive settings
hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(15);
// Additional options
hubOptions.EnableDetailedErrors = true; // For debugging
hubOptions.MaximumParallelInvocationsPerClient = 1;
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
//app.MapControllers();
//app.MapFallbackToController("Index", "Fallback");
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapStaticAssets();
app.UseCors("AllowAll");
app.UseWebSockets();
// adding UseAuthentication into our pipeline and this should come before UseAuthorization
// Authentication verifies the identity of a user or service, and authorization determines their access rights.
app.UseAuthentication();
app.UseAuthorization();
// Add to your app configuration (before MapControllers)
app.MapHub("/friendHub");
app.MapControllers();
app.MapGet("/test", () => "Server is running.");
app.MapFallbackToFile("/index.html");
#region ContextSeed
using var scope = app.Services.CreateScope();
try
{
var contextSeedService = scope.ServiceProvider.GetService();
await contextSeedService.InitializeContextAsync();
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetService();
logger.LogError(ex.Message, "Failed to initialize and seed the database");
}
#endregion
app.Run();
And here is my signalr.service.ts:
import { Injectable } from '@angular/core';
import * as signalR from '@microsoft/signalr';
import { Subject, Observable, BehaviorSubject } from 'rxjs';
import { environment } from 'src/environments/environment';
import { AccountService } from '../account/account.service';
import { MessageDto } from '../models/messageDto';
@Injectable({ providedIn: 'root' })
export class SignalRService {
private hubURL = "https://localhost:5210/friendHub";
public hubConnection!: signalR.HubConnection;
private connectionState = new BehaviorSubject(signalR.HubConnectionState.Disconnected);
private notificationHandlers = new Map void>();
private messageQueue: { methodName: string, args: any[] }[] = [];
private isExplicitlyDisconnected = false;
public connectionState$ = this.connectionState.asObservable();
constructor(private authService: AccountService) {
this.createConnection();
this.startConnection();
}
private createConnection(): void {
this.hubConnection = new signalR.HubConnectionBuilder()
//.withUrl(`${this.hubURL}?access_token=${this.authService.getJWT() ?? ''}`)
.withUrl(this.hubURL, {
accessTokenFactory: () => {
const token = this.authService.getJWT();
console.log('Using token:', token); // Debug logging
return token ?? '';
},
skipNegotiation: true, // Keep negotiation enabled for now
transport: signalR.HttpTransportType.WebSockets
})
// .withUrl(this.hubURL, {
// accessTokenFactory: () => this.authService.getJWT() ?? '',
// skipNegotiation: true,
// transport: signalR.HttpTransportType.WebSockets
// })
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: retryContext => {
return Math.min(retryContext.previousRetryCount * 2000, 10000);
}
})
.configureLogging(signalR.LogLevel.Warning)
.build();
this.registerConnectionEvents();
}
private registerConnectionEvents(): void {
this.hubConnection.onreconnecting(error => {
console.log(`SignalR reconnecting due to ${error?.message}`);
this.connectionState.next(signalR.HubConnectionState.Reconnecting);
});
this.hubConnection.onreconnected(connectionId => {
console.log(`SignalR reconnected. New connection ID: ${connectionId}`);
this.connectionState.next(signalR.HubConnectionState.Connected);
this.processMessageQueue();
});
this.hubConnection.onclose(error => {
console.log(`SignalR connection closed. ${error?.message}`);
this.connectionState.next(signalR.HubConnectionState.Disconnected);
if (!this.isExplicitlyDisconnected && error) {
setTimeout(() => this.startConnection(), 5000);
}
});
}
public async startConnection(): Promise {
if (this.hubConnection?.state === signalR.HubConnectionState.Disconnected) {
this.isExplicitlyDisconnected = false;
try {
await this.hubConnection.start();
console.log('SignalR Connected');
this.connectionState.next(signalR.HubConnectionState.Connected);
this.joinUserGroup();
this.processMessageQueue();
} catch (err) {
console.error('Error starting SignalR connection:', err);
this.connectionState.next(signalR.HubConnectionState.Disconnected);
setTimeout(() => this.startConnection(), 5000);
}
}
}
private async processMessageQueue(): Promise {
while (this.messageQueue.length > 0 &&
this.hubConnection.state === signalR.HubConnectionState.Connected) {
const message = this.messageQueue.shift();
try {
await this.hubConnection.invoke(message!.methodName, ...message!.args);
} catch (err) {
console.error(`Error processing queued message ${message!.methodName}:`, err);
this.messageQueue.unshift(message!); // Put it back if failed
break;
}
}
}
private joinUserGroup(): void {
this.invokeWithQueue('JoinUserGroup').catch(err =>
console.error('Error joining user group:', err));
}
public async invokeWithQueue(methodName: string, ...args: any[]): Promise {
if (this.hubConnection.state === signalR.HubConnectionState.Connected) {
try {
return await this.hubConnection.invoke(methodName, ...args);
} catch (err) {
console.error(`Error invoking ${methodName}:`, err);
throw err;
}
} else {
console.log(`Queueing ${methodName} (connection state: ${this.hubConnection.state})`);
this.messageQueue.push({ methodName, args });
if (this.hubConnection.state === signalR.HubConnectionState.Disconnected) {
await this.startConnection();
}
return Promise.resolve();
}
}
public on(methodName: string, callback: (data: T) => void): void {
if (this.notificationHandlers.has(methodName)) {
const handler = this.notificationHandlers.get(methodName);
if (handler) {
this.hubConnection.off(methodName, handler);
}
}
this.notificationHandlers.set(methodName, callback);
this.hubConnection.on(methodName, callback);
}
public off(methodName: string): void {
const handler = this.notificationHandlers.get(methodName);
if (handler) {
this.hubConnection.off(methodName, handler);
this.notificationHandlers.delete(methodName);
}
}
public async stopConnection(): Promise {
this.isExplicitlyDisconnected = true;
try {
await this.hubConnection.stop();
} catch (err) {
console.error('Error stopping SignalR connection:', err);
}
}
addFriendRequestListener(callback: (request: { fromUserName: string }) => void) {
this.on('ReceiveFriendRequest', callback);
}
notifyMessageRead(messageId: string): void {
this.invokeWithQueue('MarkAsRead', messageId)
.catch(err => console.error('Error notifying message read:', err));
}
sendMessage(messageDto: MessageDto): void {
this.invokeWithQueue('SendMessageToFriend', messageDto)
.catch(err => console.error('Error sending message:', err));
}
}
Подробнее здесь: https://stackoverflow.com/questions/797 ... ded-to-hub
Мобильная версия