🧱 Grpc service basis
This commit is contained in:
42
DysonNetwork.Pusher/Connection/ClientTypeMiddleware.cs
Normal file
42
DysonNetwork.Pusher/Connection/ClientTypeMiddleware.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace DysonNetwork.Pusher.Connection;
|
||||
|
||||
public class ClientTypeMiddleware(RequestDelegate next)
|
||||
{
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
var headers = context.Request.Headers;
|
||||
bool isWebPage;
|
||||
|
||||
// Priority 1: Check for custom header
|
||||
if (headers.TryGetValue("X-Client", out var clientType))
|
||||
{
|
||||
isWebPage = clientType.ToString().Length == 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var userAgent = headers.UserAgent.ToString();
|
||||
var accept = headers.Accept.ToString();
|
||||
|
||||
// Priority 2: Check known app User-Agent (backward compatibility)
|
||||
if (!string.IsNullOrEmpty(userAgent) && userAgent.Contains("Solian"))
|
||||
isWebPage = false;
|
||||
// Priority 3: Accept header can help infer intent
|
||||
else if (!string.IsNullOrEmpty(accept) && accept.Contains("text/html"))
|
||||
isWebPage = true;
|
||||
else if (!string.IsNullOrEmpty(accept) && accept.Contains("application/json"))
|
||||
isWebPage = false;
|
||||
else
|
||||
isWebPage = true;
|
||||
}
|
||||
|
||||
context.Items["IsWebPage"] = isWebPage;
|
||||
|
||||
if (!isWebPage && context.Request.Path != "/ws" && !context.Request.Path.StartsWithSegments("/api"))
|
||||
context.Response.Redirect(
|
||||
$"/api{context.Request.Path.Value}{context.Request.QueryString.Value}",
|
||||
permanent: false
|
||||
);
|
||||
else
|
||||
await next(context);
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace DysonNetwork.Pusher.Connection;
|
||||
|
||||
public interface IWebSocketPacketHandler
|
||||
{
|
||||
string PacketType { get; }
|
||||
Task HandleAsync(Account currentUser, string deviceId, WebSocketPacket packet, WebSocket socket, WebSocketService srv);
|
||||
}
|
108
DysonNetwork.Pusher/Connection/WebSocketController.cs
Normal file
108
DysonNetwork.Pusher/Connection/WebSocketController.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Internal;
|
||||
using Swashbuckle.AspNetCore.Annotations;
|
||||
|
||||
namespace DysonNetwork.Pusher.Connection;
|
||||
|
||||
[ApiController]
|
||||
[Route("/ws")]
|
||||
public class WebSocketController(WebSocketService ws, ILogger<WebSocketContext> logger) : ControllerBase
|
||||
{
|
||||
[Route("/ws")]
|
||||
[Authorize]
|
||||
[SwaggerIgnore]
|
||||
public async Task TheGateway()
|
||||
{
|
||||
HttpContext.Items.TryGetValue("CurrentUser", out var currentUserValue);
|
||||
HttpContext.Items.TryGetValue("CurrentSession", out var currentSessionValue);
|
||||
if (currentUserValue is not Account.Account currentUser ||
|
||||
currentSessionValue is not Auth.Session currentSession)
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
return;
|
||||
}
|
||||
|
||||
var accountId = currentUser.Id;
|
||||
var deviceId = currentSession.Challenge.DeviceId;
|
||||
|
||||
if (string.IsNullOrEmpty(deviceId))
|
||||
{
|
||||
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
return;
|
||||
}
|
||||
|
||||
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||
var cts = new CancellationTokenSource();
|
||||
var connectionKey = (accountId, deviceId);
|
||||
|
||||
if (!ws.TryAdd(connectionKey, webSocket, cts))
|
||||
{
|
||||
await webSocket.CloseAsync(
|
||||
WebSocketCloseStatus.InternalServerError,
|
||||
"Failed to establish connection.",
|
||||
CancellationToken.None
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
$"Connection established with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}");
|
||||
|
||||
try
|
||||
{
|
||||
await _ConnectionEventLoop(deviceId, currentUser, webSocket, cts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"WebSocket Error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
ws.Disconnect(connectionKey);
|
||||
logger.LogInformation(
|
||||
$"Connection disconnected with user @{currentUser.Name}#{currentUser.Id} and device #{deviceId}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _ConnectionEventLoop(
|
||||
string deviceId,
|
||||
Account.Account currentUser,
|
||||
WebSocket webSocket,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var connectionKey = (AccountId: currentUser.Id, DeviceId: deviceId);
|
||||
|
||||
var buffer = new byte[1024 * 4];
|
||||
try
|
||||
{
|
||||
var receiveResult = await webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cancellationToken
|
||||
);
|
||||
while (!receiveResult.CloseStatus.HasValue)
|
||||
{
|
||||
receiveResult = await webSocket.ReceiveAsync(
|
||||
new ArraySegment<byte>(buffer),
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
var packet = WebSocketPacket.FromBytes(buffer[..receiveResult.Count]);
|
||||
_ = ws.HandlePacket(currentUser, connectionKey.DeviceId, packet, webSocket);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (
|
||||
webSocket.State != WebSocketState.Closed
|
||||
&& webSocket.State != WebSocketState.Aborted
|
||||
)
|
||||
{
|
||||
ws.Disconnect(connectionKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
DysonNetwork.Pusher/Connection/WebSocketPacket.cs
Normal file
72
DysonNetwork.Pusher/Connection/WebSocketPacket.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using System.Text.Json;
|
||||
using NodaTime;
|
||||
using NodaTime.Serialization.SystemTextJson;
|
||||
|
||||
public class WebSocketPacketType
|
||||
{
|
||||
public const string Error = "error";
|
||||
public const string MessageNew = "messages.new";
|
||||
public const string MessageUpdate = "messages.update";
|
||||
public const string MessageDelete = "messages.delete";
|
||||
public const string CallParticipantsUpdate = "call.participants.update";
|
||||
}
|
||||
|
||||
public class WebSocketPacket
|
||||
{
|
||||
public string Type { get; set; } = null!;
|
||||
public object Data { get; set; } = null!;
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a WebSocketPacket from raw WebSocket message bytes
|
||||
/// </summary>
|
||||
/// <param name="bytes">Raw WebSocket message bytes</param>
|
||||
/// <returns>Deserialized WebSocketPacket</returns>
|
||||
public static WebSocketPacket FromBytes(byte[] bytes)
|
||||
{
|
||||
var json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
var jsonOpts = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
return JsonSerializer.Deserialize<WebSocketPacket>(json, jsonOpts) ??
|
||||
throw new JsonException("Failed to deserialize WebSocketPacket");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the Data property to the specified type T
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Target type to deserialize to</typeparam>
|
||||
/// <returns>Deserialized data of type T</returns>
|
||||
public T? GetData<T>()
|
||||
{
|
||||
if (Data is T typedData)
|
||||
return typedData;
|
||||
|
||||
var jsonOpts = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
};
|
||||
return JsonSerializer.Deserialize<T>(
|
||||
JsonSerializer.Serialize(Data, jsonOpts),
|
||||
jsonOpts
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes this WebSocketPacket to a byte array for sending over WebSocket
|
||||
/// </summary>
|
||||
/// <returns>Byte array representation of the packet</returns>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var jsonOpts = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DictionaryKeyPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
}.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);
|
||||
var json = JsonSerializer.Serialize(this, jsonOpts);
|
||||
return System.Text.Encoding.UTF8.GetBytes(json);
|
||||
}
|
||||
}
|
129
DysonNetwork.Pusher/Connection/WebSocketService.cs
Normal file
129
DysonNetwork.Pusher/Connection/WebSocketService.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
|
||||
namespace DysonNetwork.Pusher.Connection;
|
||||
|
||||
public class WebSocketService
|
||||
{
|
||||
private readonly IDictionary<string, IWebSocketPacketHandler> _handlerMap;
|
||||
|
||||
public WebSocketService(IEnumerable<IWebSocketPacketHandler> handlers)
|
||||
{
|
||||
_handlerMap = handlers.ToDictionary(h => h.PacketType);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<
|
||||
(Guid AccountId, string DeviceId),
|
||||
(WebSocket Socket, CancellationTokenSource Cts)
|
||||
> ActiveConnections = new();
|
||||
|
||||
private static readonly ConcurrentDictionary<string, string> ActiveSubscriptions = new(); // deviceId -> chatRoomId
|
||||
|
||||
public void SubscribeToChatRoom(string chatRoomId, string deviceId)
|
||||
{
|
||||
ActiveSubscriptions[deviceId] = chatRoomId;
|
||||
}
|
||||
|
||||
public void UnsubscribeFromChatRoom(string deviceId)
|
||||
{
|
||||
ActiveSubscriptions.TryRemove(deviceId, out _);
|
||||
}
|
||||
|
||||
public bool IsUserSubscribedToChatRoom(Guid accountId, string chatRoomId)
|
||||
{
|
||||
var userDeviceIds = ActiveConnections.Keys.Where(k => k.AccountId == accountId).Select(k => k.DeviceId);
|
||||
foreach (var deviceId in userDeviceIds)
|
||||
{
|
||||
if (ActiveSubscriptions.TryGetValue(deviceId, out var subscribedChatRoomId) && subscribedChatRoomId == chatRoomId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryAdd(
|
||||
(Guid AccountId, string DeviceId) key,
|
||||
WebSocket socket,
|
||||
CancellationTokenSource cts
|
||||
)
|
||||
{
|
||||
if (ActiveConnections.TryGetValue(key, out _))
|
||||
Disconnect(key,
|
||||
"Just connected somewhere else with the same identifier."); // Disconnect the previous one using the same identifier
|
||||
return ActiveConnections.TryAdd(key, (socket, cts));
|
||||
}
|
||||
|
||||
public void Disconnect((Guid AccountId, string DeviceId) key, string? reason = null)
|
||||
{
|
||||
if (!ActiveConnections.TryGetValue(key, out var data)) return;
|
||||
data.Socket.CloseAsync(
|
||||
WebSocketCloseStatus.NormalClosure,
|
||||
reason ?? "Server just decided to disconnect.",
|
||||
CancellationToken.None
|
||||
);
|
||||
data.Cts.Cancel();
|
||||
ActiveConnections.TryRemove(key, out _);
|
||||
UnsubscribeFromChatRoom(key.DeviceId);
|
||||
}
|
||||
|
||||
public bool GetAccountIsConnected(Guid accountId)
|
||||
{
|
||||
return ActiveConnections.Any(c => c.Key.AccountId == accountId);
|
||||
}
|
||||
|
||||
public void SendPacketToAccount(Guid userId, WebSocketPacket packet)
|
||||
{
|
||||
var connections = ActiveConnections.Where(c => c.Key.AccountId == userId);
|
||||
var packetBytes = packet.ToBytes();
|
||||
var segment = new ArraySegment<byte>(packetBytes);
|
||||
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
connection.Value.Socket.SendAsync(
|
||||
segment,
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void SendPacketToDevice(string deviceId, WebSocketPacket packet)
|
||||
{
|
||||
var connections = ActiveConnections.Where(c => c.Key.DeviceId == deviceId);
|
||||
var packetBytes = packet.ToBytes();
|
||||
var segment = new ArraySegment<byte>(packetBytes);
|
||||
|
||||
foreach (var connection in connections)
|
||||
{
|
||||
connection.Value.Socket.SendAsync(
|
||||
segment,
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandlePacket(Account.Account currentUser, string deviceId, WebSocketPacket packet,
|
||||
WebSocket socket)
|
||||
{
|
||||
if (_handlerMap.TryGetValue(packet.Type, out var handler))
|
||||
{
|
||||
await handler.HandleAsync(currentUser, deviceId, packet, socket, this);
|
||||
return;
|
||||
}
|
||||
|
||||
await socket.SendAsync(
|
||||
new ArraySegment<byte>(new WebSocketPacket
|
||||
{
|
||||
Type = WebSocketPacketType.Error,
|
||||
ErrorMessage = $"Unprocessable packet: {packet.Type}"
|
||||
}.ToBytes()),
|
||||
WebSocketMessageType.Binary,
|
||||
true,
|
||||
CancellationToken.None
|
||||
);
|
||||
}
|
||||
}
|
23
DysonNetwork.Pusher/Dockerfile
Normal file
23
DysonNetwork.Pusher/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
USER $APP_UID
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
EXPOSE 8081
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["DysonNetwork.Pusher/DysonNetwork.Pusher.csproj", "DysonNetwork.Pusher/"]
|
||||
RUN dotnet restore "DysonNetwork.Pusher/DysonNetwork.Pusher.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/DysonNetwork.Pusher"
|
||||
RUN dotnet build "./DysonNetwork.Pusher.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./DysonNetwork.Pusher.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
ENTRYPOINT ["dotnet", "DysonNetwork.Pusher.dll"]
|
21
DysonNetwork.Pusher/DysonNetwork.Pusher.csproj
Normal file
21
DysonNetwork.Pusher/DysonNetwork.Pusher.csproj
Normal file
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Grpc.AspNetCore.Server" Version="2.71.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\.dockerignore">
|
||||
<Link>.dockerignore</Link>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
23
DysonNetwork.Pusher/Program.cs
Normal file
23
DysonNetwork.Pusher/Program.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
|
||||
builder.Services.AddControllers();
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
23
DysonNetwork.Pusher/Properties/launchSettings.json
Normal file
23
DysonNetwork.Pusher/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5212",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7259;http://localhost:5212",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
129
DysonNetwork.Pusher/appsettings.json
Normal file
129
DysonNetwork.Pusher/appsettings.json
Normal file
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"Debug": true,
|
||||
"BaseUrl": "http://localhost:5071",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"App": "Host=localhost;Port=5432;Database=dyson_network;Username=postgres;Password=postgres;Include Error Detail=True;Maximum Pool Size=20;Connection Idle Lifetime=60",
|
||||
"FastRetrieve": "localhost:6379"
|
||||
},
|
||||
"Authentication": {
|
||||
"Schemes": {
|
||||
"Bearer": {
|
||||
"ValidAudiences": [
|
||||
"http://localhost:5071",
|
||||
"https://localhost:7099"
|
||||
],
|
||||
"ValidIssuer": "solar-network"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AuthToken": {
|
||||
"PublicKeyPath": "Keys/PublicKey.pem",
|
||||
"PrivateKeyPath": "Keys/PrivateKey.pem"
|
||||
},
|
||||
"OidcProvider": {
|
||||
"IssuerUri": "https://nt.solian.app",
|
||||
"PublicKeyPath": "Keys/PublicKey.pem",
|
||||
"PrivateKeyPath": "Keys/PrivateKey.pem",
|
||||
"AccessTokenLifetime": "01:00:00",
|
||||
"RefreshTokenLifetime": "30.00:00:00",
|
||||
"AuthorizationCodeLifetime": "00:30:00",
|
||||
"RequireHttpsMetadata": true
|
||||
},
|
||||
"Tus": {
|
||||
"StorePath": "Uploads"
|
||||
},
|
||||
"Storage": {
|
||||
"PreferredRemote": "minio",
|
||||
"Remote": [
|
||||
{
|
||||
"Id": "minio",
|
||||
"Label": "Minio",
|
||||
"Region": "auto",
|
||||
"Bucket": "solar-network-development",
|
||||
"Endpoint": "localhost:9000",
|
||||
"SecretId": "littlesheep",
|
||||
"SecretKey": "password",
|
||||
"EnabledSigned": true,
|
||||
"EnableSsl": false
|
||||
},
|
||||
{
|
||||
"Id": "cloudflare",
|
||||
"Label": "Cloudflare R2",
|
||||
"Region": "auto",
|
||||
"Bucket": "solar-network",
|
||||
"Endpoint": "0a70a6d1b7128888c823359d0008f4e1.r2.cloudflarestorage.com",
|
||||
"SecretId": "8ff5d06c7b1639829d60bc6838a542e6",
|
||||
"SecretKey": "fd58158c5201be16d1872c9209d9cf199421dae3c2f9972f94b2305976580d67",
|
||||
"EnableSigned": true,
|
||||
"EnableSsl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"Captcha": {
|
||||
"Provider": "cloudflare",
|
||||
"ApiKey": "0x4AAAAAABCDUdOujj4feOb_",
|
||||
"ApiSecret": "0x4AAAAAABCDUWABiJQweqlB7tYq-IqIm8U"
|
||||
},
|
||||
"Notifications": {
|
||||
"Topic": "dev.solsynth.solian",
|
||||
"Endpoint": "http://localhost:8088"
|
||||
},
|
||||
"Email": {
|
||||
"Server": "smtp4dev.orb.local",
|
||||
"Port": 25,
|
||||
"UseSsl": false,
|
||||
"Username": "no-reply@mail.solsynth.dev",
|
||||
"Password": "password",
|
||||
"FromAddress": "no-reply@mail.solsynth.dev",
|
||||
"FromName": "Alphabot",
|
||||
"SubjectPrefix": "Solar Network"
|
||||
},
|
||||
"RealtimeChat": {
|
||||
"Endpoint": "https://solar-network-im44o8gq.livekit.cloud",
|
||||
"ApiKey": "APIs6TiL8wj3A4j",
|
||||
"ApiSecret": "SffxRneIwTnlHPtEf3zicmmv3LUEl7xXael4PvWZrEhE"
|
||||
},
|
||||
"GeoIp": {
|
||||
"DatabasePath": "./Keys/GeoLite2-City.mmdb"
|
||||
},
|
||||
"Oidc": {
|
||||
"Google": {
|
||||
"ClientId": "961776991058-963m1qin2vtp8fv693b5fdrab5hmpl89.apps.googleusercontent.com",
|
||||
"ClientSecret": ""
|
||||
},
|
||||
"Apple": {
|
||||
"ClientId": "dev.solsynth.solian",
|
||||
"TeamId": "W7HPZ53V6B",
|
||||
"KeyId": "B668YP4KBG",
|
||||
"PrivateKeyPath": "./Keys/Solarpass.p8"
|
||||
},
|
||||
"Microsoft": {
|
||||
"ClientId": "YOUR_MICROSOFT_CLIENT_ID",
|
||||
"ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET",
|
||||
"DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT"
|
||||
}
|
||||
},
|
||||
"Payment": {
|
||||
"Auth": {
|
||||
"Afdian": "<token here>"
|
||||
},
|
||||
"Subscriptions": {
|
||||
"Afdian": {
|
||||
"7d17aae23c9611f0b5705254001e7c00": "solian.stellar.primary",
|
||||
"7dfae4743c9611f0b3a55254001e7c00": "solian.stellar.nova",
|
||||
"141713ee3d6211f085b352540025c377": "solian.stellar.supernova"
|
||||
}
|
||||
}
|
||||
},
|
||||
"KnownProxies": [
|
||||
"127.0.0.1",
|
||||
"::1"
|
||||
]
|
||||
}
|
Reference in New Issue
Block a user