SignalR auth working

This commit is contained in:
Fergal Moran
2018-05-09 09:48:07 +01:00
parent 8725d632f0
commit fd22da93b8
54 changed files with 333 additions and 167 deletions

View File

@@ -24,6 +24,7 @@
"@angular/platform-browser": "6.0.0",
"@angular/platform-browser-dynamic": "6.0.0",
"@angular/router": "6.0.0",
"@aspnet/signalr": "^1.0.0-rtm-30751",
"@ngrx/effects": "^5.1.0",
"@ngrx/store": "^5.1.0",
"@ngrx/store-devtools": "^5.1.0",

View File

@@ -23,12 +23,29 @@ export const ADD_SUCCESS = '[Chat] Add Chat Success';
export const ADD_FAIL = '[Chat] Add Chat Fail';
export class AddAction implements Action {
readonly type = ADD;
constructor(public payload: ChatModel) {
}
constructor(public payload: ChatModel) {}
}
export class AddSuccessAction implements Action {
readonly type = ADD_SUCCESS;
constructor(public payload: ChatModel) {}
}
//#endregion
//#region Add
// the receive action is for messages from signalR
// we don't want to initiate a POST to the server.
export const RECEIVE = '[Chat] Receive Chat';
export const RECEIVE_SUCCESS = '[Chat] Receive Chat Success';
export const RECEIVE_FAIL = '[Chat] Receive Chat Fail';
export class ReceiveAction implements Action {
readonly type = RECEIVE;
constructor(public payload: ChatModel) {
console.log('chat.actions', 'RECEIVE', payload);
}
}
export class ReceiveSuccessAction implements Action {
readonly type = RECEIVE_SUCCESS;
constructor(public payload: ChatModel) {
console.log('chat.actions', 'RECEIVE_SUCESS', payload);
}
}
//#endregion
@@ -37,5 +54,6 @@ export type Actions =
| LoadSuccessAction
| LoadFailAction
| AddAction
| AddSuccessAction;
| AddSuccessAction
| ReceiveAction
| ReceiveSuccessAction;

View File

@@ -58,13 +58,11 @@ export class AppComponent implements OnInit {
const chatterChannel = `${p.id}`;
this._signalrService
.init('userupdates')
.then((r) => {
this._signalrService.connection.on(
chatterChannel,
(result) => {
this._toastyService.info(result);
}
);
.then((listener) => {
listener.on<string>(chatterChannel)
.subscribe(result => {
this._toastyService.info(result);
});
})
.catch((err) => {
console.error(

View File

@@ -156,7 +156,7 @@ export function provideConfig() {
authDomain: 'podnoms-api.firebaseapp.com',
databaseURL: 'https://podnoms-api.firebaseio.com',
projectId: 'podnoms-api',
storageBucket: '',
storageBucket: 'podnoms-api.appspot.com',
messagingSenderId: '357461672895'
}),
AngularFireDatabaseModule,

View File

@@ -25,8 +25,9 @@
</div>
</div>
<div class="chat-controls" *ngIf="!loading">
<input class="message-input" placeholder="Type your message here..." type="text" #message (keyup.enter)="sendMessage(message)">
<div class="chat-controls">
<input #message class="message-input" placeholder="Type your message and press enter..." type="text"
(keyup.enter)="sendMessage(message)">
<div class="chat-extra">
<a href="#">
<span class="extra-tooltip">Attach Document</span>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { ProfileService } from '../../services/profile.service';
import { SignalRService } from '../../services/signalr.service';
import { ProfileModel } from 'app/models/profile.model';
@@ -15,11 +15,17 @@ import { Observable } from 'rxjs/Observable';
templateUrl: './chat-widget.component.html',
styleUrls: ['./chat-widget.component.css']
})
export class ChatWidgetComponent implements OnInit {
export class ChatWidgetComponent {
chatActive: boolean = false;
loading: boolean = false;
user: ProfileModel;
private user: ProfileModel;
messages$: Observable<ChatModel[]>;
private messageEl: ElementRef;
// have to handle ViewChild like this as it's hidden with ngIf
@ViewChild('message')
set content(content: ElementRef) {
this.messageEl = content;
}
constructor(
private _profileService: ProfileService,
@@ -28,25 +34,17 @@ export class ChatWidgetComponent implements OnInit {
) {
this._profileService.getProfile().subscribe((p) => (this.user = p));
this.messages$ = _store.select(fromChat.getChat);
this.messages$.subscribe((r) => {
if (r.length != 0) {
this.chatActive = true;
}
});
}
ngOnInit() {}
togglePopup() {
this.chatActive = !this.chatActive;
if (this.chatActive && this.user) {
this.loading = true;
this._signalRService.init('chat').then(() => {
this.loading = false;
this._signalRService.connection.on('SendMessage', (message) => {
console.log(
'chat-widget.component',
'SendMessage',
message
);
});
});
if (this.chatActive) {
setTimeout(() => this.messageEl.nativeElement.focus(), 0);
}
}

View File

@@ -10,6 +10,7 @@ import * as fromEntriesActions from 'app/actions/entries.actions';
import { PodcastService } from '../../../services/podcast.service';
import { AudioService } from 'app/services/audio.service';
import { Observable } from 'rxjs/Observable';
import { AudioProcessingMessageModel } from 'app/models/audioprocessingmessage.model';
@Component({
selector: '[app-entry-list-item]',
@@ -39,23 +40,22 @@ export class EntryListItemComponent implements OnInit {
) {
this._signalRService
.init('audioprocessing')
.then(() => {
.then((listener) => {
const updateChannel: string = `${
this.entry.uid
}__progress_update`;
const processedChannel: string = `${
this.entry.uid
}__info_processed`;
this._signalRService.connection.on(
updateChannel,
(result) => {
listener
.on<AudioProcessingMessageModel>(updateChannel)
.subscribe((result) => {
this.percentageProcessed = result.percentage;
this.currentSpeed = result.currentSpeed;
}
);
this._signalRService.connection.on(
processedChannel,
(result) => {
});
listener
.on<PodcastEntryModel>(processedChannel)
.subscribe((result) => {
this.entry = result;
if (this.entry.processingStatus === 'Processed') {
// only update the store when we're finished.
@@ -65,8 +65,7 @@ export class EntryListItemComponent implements OnInit {
)
);
}
}
);
});
})
.catch((err) =>
console.error(

View File

@@ -1,7 +1,4 @@
import {
LoadAction,
LOAD_FAIL
} from './../actions/chat.actions';
import { LoadAction, LOAD_FAIL } from './../actions/chat.actions';
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { Observable } from 'rxjs/Observable';
@@ -22,17 +19,26 @@ export class ChatEffects {
.switchMap((payload: chat.LoadAction) =>
this._service
.get()
.map(res => ({ type: chat.LOAD_SUCCESS, payload: res }))
.catch(err => {
.map((res) => ({ type: chat.LOAD_SUCCESS, payload: res }))
.catch((err) => {
console.error('ChatEffects', 'get$', err);
return Observable.of({ type: chat.LOAD_FAIL });
})
);
@Effect()
put$ = this.actions$
.ofType(chat.ADD)
.switchMap((action: chat.AddAction) => this._service.send(action.payload))
.map(res => ({ type: chat.ADD_SUCCESS, payload: res }));
@Effect()
put$ = this.actions$
.ofType(chat.ADD)
.switchMap((action: chat.AddAction) =>
this._service.send(action.payload)
)
.map((res) => ({ type: chat.ADD_SUCCESS, payload: res }));
@Effect()
receive$ = this.actions$
.ofType(chat.RECEIVE)
.map((res: chat.ReceiveAction) => ({ type: chat.RECEIVE_SUCCESS, payload: res.payload }));
// receive$ = this.actions$.ofType(chat.RECEIVE).map((res) => {
// return { type: chat.RECEIVE_SUCCESS, payload: res };
// });
constructor(private _service: ChatService, private actions$: Actions) {}
}

View File

@@ -0,0 +1,6 @@
export class AudioProcessingMessageModel {
percentage: number;
totalSize: string;
currentSpeed: string;
eTA: string;
}

View File

@@ -33,7 +33,8 @@ export function reducer(state = initialState, action: chat.Actions): State {
loading: false
};
}
case chat.ADD_SUCCESS: {
case chat.ADD_SUCCESS:
case chat.RECEIVE_SUCCESS: {
const newResults = _.clone(state.result);
newResults.push(action.payload);
const newState = {

View File

@@ -4,11 +4,35 @@ import { Observable } from 'rxjs/Observable';
import { environment } from 'environments/environment';
import { ChatModel } from 'app/models/chat.model';
import { HttpClient } from '@angular/common/http';
import { SignalRService } from './signalr.service';
import { Subject } from 'rxjs';
import * as fromChat from 'app/reducers';
import * as fromChatActions from 'app/actions/chat.actions';
import { ApplicationState } from '../store';
import { Store } from '@ngrx/store';
@Injectable()
export class ChatService extends BaseService {
constructor(private _http: HttpClient) {
constructor(
private _http: HttpClient,
private _signalRService: SignalRService,
private _store: Store<ApplicationState>
) {
super();
this._signalRService.init('chat').then((listener) => {
listener
.on<ChatModel>('SendMessage')
.subscribe((message: ChatModel) => {
console.log(
'chat-widget.component',
'SendMessage',
message
);
this._store.dispatch(
new fromChatActions.ReceiveAction(message)
);
});
});
}
get(): Observable<ChatModel[]> {

View File

@@ -18,7 +18,7 @@ export class MessagingService {
private _pushRegistrationServer: PushRegistrationService
) {
this.messaging.usePublicVapidKey(
'BKyhUqIVZLauKNA-DXPXbIVLj5XiWurHbRV_0Rd3BOjY5cU9GOrd5ptXVJ2CNExxdveKYzZevrep2CflKeqkyqo'
'BP05eVFzqeEh54DaL3TOe6x_8UFs60nw_gfSrI5tdILjb5VnHwas0n7c_075tsc1w5fm87u9d4Dawj_YN13PSAI'
);
}

View File

@@ -1,31 +1,37 @@
import { PodnomsAuthService } from './podnoms-auth.service';
import { Injectable } from '@angular/core';
import {
HubConnection,
HubConnectionBuilder,
JsonHubProtocol,
LogLevel
} from '@aspnet/signalr';
import { HubConnection, HubConnectionBuilder, LogLevel } from '@aspnet/signalr';
import { environment } from 'environments/environment';
import { Observable, Subscriber } from 'rxjs';
@Injectable()
export class SignalRService {
public connection: HubConnection;
private _connected: boolean = false;
private _connection: HubConnection;
constructor(private _auth: PodnomsAuthService) {}
public init(hub: string): Promise<void> {
const url = `${environment.SIGNALR_HOST}/hubs/${hub}`;
public init(hub: string): Promise<SignalRService> {
return new Promise((resolve) => {
const url = `${environment.SIGNALR_HOST}/hubs/${hub}`;
const token = this._auth.getToken();
this._connection = new HubConnectionBuilder()
.configureLogging(LogLevel.Debug)
.withUrl(url + '?token=' + token)
.build();
resolve(this);
});
}
const token = this._auth.getToken();
const options: any = {
transport: 0
};
this.connection = new HubConnectionBuilder()
.configureLogging(LogLevel.Error)
.withUrl(url + '?token=' + token, options)
.withHubProtocol(new JsonHubProtocol())
.build();
return this.connection.start();
public on<T>(channel: string): Observable<T> {
const listener = new Observable<T>((subscriber: Subscriber<T>) => {
this._connection.on(channel, (message) => {
const result: T = message as T;
subscriber.next(result);
});
});
if (!this._connected) {
this._connection.start().then(() => (this._connected = true));
}
return listener;
}
}

View File

@@ -32,9 +32,6 @@
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->
<!-- Begin Crisp -->
<script type="text/javascript">window.$crisp = []; window.CRISP_WEBSITE_ID = "af7d6aa7-0cb8-4f74-89aa-7f3742058e85"; (function () { d = document; s = d.createElement("script"); s.src = "https://client.crisp.chat/l.js"; s.async = 1; d.getElementsByTagName("head")[0].appendChild(s); })();</script>
<!-- End Crisp -->
</body>
</html>

View File

@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Persistence;
using PodNoms.Api.Providers;

View File

@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Services;
using PodNoms.Api.Services.Auth;

View File

@@ -1,10 +1,12 @@
using System.Linq;
using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using PodNoms.Api.Services.Auth;
[Authorize]
public class BaseAuthController : Controller {
private readonly ClaimsPrincipal _caller;
protected readonly UserManager<ApplicationUser> _userManager;

View File

@@ -1,29 +1,40 @@
using System.Threading.Tasks;
using Lib.Net.Http.WebPush;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Services;
using PodNoms.Api.Services.Auth;
using PodNoms.Api.Services.Hubs;
using PodNoms.Api.Services.Push;
using WebPush = Lib.Net.Http.WebPush;
namespace PodNoms.Api.Controllers {
[Route("[controller]")]
[Authorize]
public class ChatController : BaseAuthController {
private readonly HubLifetimeManager<ChatHub> _hub;
private readonly ISupportChatService _supportChatService;
public ChatController(IHttpContextAccessor contextAccessor, UserManager<ApplicationUser> userManager,
HubLifetimeManager<ChatHub> chatHubContext) :
ISupportChatService supportChatService) :
base(contextAccessor, userManager) {
this._hub = chatHubContext;
this._supportChatService = supportChatService;
}
[HttpPost]
public async Task<ActionResult<ChatViewModel>> Post([FromBody]ChatViewModel message) {
await this._hub.SendAllAsync("SendMessage", new object[] { message.Message });
return Ok(message);
//need to lookup the current support host and notify them
message.FromUserName = _applicationUser.FullName;
message.FromUserId = _applicationUser.Id;
if (await _supportChatService.InitiateSupportRequest(_userId, message)) {
return Ok(message);
}
return Accepted(message);
}
}
}

View File

@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Auth;
using PodNoms.Api.Services.Downloader;
@@ -24,7 +25,7 @@ namespace PodNoms.Api.Controllers {
public class DebugController : BaseAuthController {
private readonly StorageSettings _storageSettings;
private readonly AudioFileStorageSettings _audioFileStorageSettings;
private readonly ApplicationsSettings _applicationsSettings;
private readonly HelpersSettings _helpersSettings;
private readonly ImageFileStorageSettings _imageFileStorageSettings;
private readonly HubLifetimeManager<DebugHub> _hubManager;
private readonly IPushSubscriptionStore _subscriptionStore;
@@ -34,7 +35,7 @@ namespace PodNoms.Api.Controllers {
public DebugController(IOptions<StorageSettings> settings, IOptions<AppSettings> appSettings,
HubLifetimeManager<DebugHub> hubManager,
IOptions<ApplicationsSettings> applicationsSettings,
IOptions<HelpersSettings> helpersSettings,
IOptions<AudioFileStorageSettings> audioFileStorageSettings,
IOptions<ImageFileStorageSettings> imageFileStorageSettings,
IPushSubscriptionStore subscriptionStore,
@@ -43,7 +44,7 @@ namespace PodNoms.Api.Controllers {
IHttpContextAccessor contextAccessor) : base(contextAccessor, userManager) {
this._appSettings = appSettings.Value;
this._storageSettings = settings.Value;
this._applicationsSettings = applicationsSettings.Value;
this._helpersSettings = helpersSettings.Value;
this._audioFileStorageSettings = audioFileStorageSettings.Value;
this._imageFileStorageSettings = imageFileStorageSettings.Value;
this._hubManager = hubManager;
@@ -59,8 +60,8 @@ namespace PodNoms.Api.Controllers {
CdnUrl = _storageSettings.CdnUrl,
AudioContainer = _audioFileStorageSettings.ContainerName,
ImageContainer = _imageFileStorageSettings.ContainerName,
YouTubeDlPath = _applicationsSettings.Downloader,
YouTubeDlVersion = AudioDownloader.GetVersion(_applicationsSettings.Downloader),
YouTubeDlPath = _helpersSettings.Downloader,
YouTubeDlVersion = AudioDownloader.GetVersion(_helpersSettings.Downloader),
OSVersion = System.Environment.OSVersion,
RssUrl = _appSettings.RssUrl
};

View File

@@ -7,12 +7,14 @@ using AutoMapper;
using Hangfire;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services;
@@ -24,6 +26,7 @@ using PodNoms.Api.Utils.RemoteParsers;
namespace PodNoms.Api.Controllers {
[Route("[controller]")]
[Authorize]
public class EntryController : BaseAuthController {
private readonly IPodcastRepository _podcastRepository;
private readonly IEntryRepository _repository;

View File

@@ -22,6 +22,7 @@ using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Transforms;
using SixLabors.ImageSharp.Processing.Filters;
using PodNoms.Api.Models.Settings;
namespace PodNoms.Api.Controllers {
[Authorize]
@@ -42,6 +43,7 @@ namespace PodNoms.Api.Controllers {
this._fileUploader = fileUploader;
this._imageFileStorageSettings = imageFileStorageSettings.Value;
this._repository = repository;
//this._repository = repository;
this._unitOfWork = unitOfWork;
this._mapper = mapper;
this._logger = loggerFactory.CreateLogger<ImageUploadController>();

View File

@@ -23,17 +23,14 @@ namespace PodNoms.Api.Controllers {
[Route("[controller]")]
public class PodcastController : BaseAuthController {
private readonly IPodcastRepository _repository;
private readonly IOptions<AppSettings> _settings;
private readonly IMapper _mapper;
private readonly IUnitOfWork _uow;
public PodcastController(IPodcastRepository repository, IOptions<AppSettings> options,
IMapper mapper, IUnitOfWork unitOfWork,
public PodcastController(IPodcastRepository repository, IMapper mapper, IUnitOfWork unitOfWork,
UserManager<ApplicationUser> userManager, IHttpContextAccessor contextAccessor)
: base(contextAccessor, userManager) {
this._uow = unitOfWork;
this._repository = repository;
this._settings = options;
this._mapper = mapper;
}

View File

@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels.RssViewModels;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Auth;

View File

@@ -1,29 +0,0 @@
using System.IO;
using System.Linq;
namespace PodNoms.Api.Models
{
public class ImageFileStorageSettings : FileStorageSettings
{
}
public class AudioFileStorageSettings : FileStorageSettings
{
}
public class FileStorageSettings
{
public string ContainerName { get; set; }
public long MaxUploadFileSize { get; set; }
public string[] AllowedFileTypes { get; set; }
public bool IsSupported(string fileName)
{
return AllowedFileTypes.Any(s => s == Path.GetExtension(fileName).ToLower());
}
}
public class ApplicationsSettings
{
public string Downloader { get; set; }
}
}

View File

@@ -1,8 +1,9 @@
namespace PodNoms.Api.Models {
namespace PodNoms.Api.Models.Settings {
public class AppSettings {
public string Version { get; set; }
public string SiteUrl { get; set; }
public string RssUrl { get; set; }
public string GoogleApiKey { get; set; }
public string Downloader { get; set; }
}
}

View File

@@ -0,0 +1,5 @@
namespace PodNoms.Api.Models.Settings {
public class HelpersSettings {
public string Downloader { get; set; }
}
}

View File

@@ -0,0 +1,4 @@
namespace PodNoms.Api.Models.Settings {
public class AudioFileStorageSettings : FileStorageSettings {
}
}

View File

@@ -0,0 +1,5 @@
namespace PodNoms.Api.Models.Settings {
public class ChatSettings {
public string CurrentChatUser { get; set; }
}
}

View File

@@ -1,4 +1,4 @@
namespace PodNoms.Api.Models {
namespace PodNoms.Api.Models.Settings {
public class EmailSettings {
public string ApiKey { get; set; }

View File

@@ -0,0 +1,15 @@
using System.IO;
using System.Linq;
namespace PodNoms.Api.Models.Settings {
public class FileStorageSettings {
public string ContainerName { get; set; }
public long MaxUploadFileSize { get; set; }
public string[] AllowedFileTypes { get; set; }
public bool IsSupported(string fileName) {
return AllowedFileTypes.Any(s => s == Path.GetExtension(fileName).ToLower());
}
}
}

View File

@@ -0,0 +1,4 @@
namespace PodNoms.Api.Models.Settings {
public class ImageFileStorageSettings : FileStorageSettings {
}
}

View File

@@ -1,7 +1,6 @@
namespace PodNoms.Api.Models {
namespace PodNoms.Api.Models.Settings {
public class StorageSettings {
public string ConnectionString { get; set; }
public string CdnUrl { get; set; }
}
}

View File

@@ -1,5 +1,9 @@
namespace PodNoms.Api.Models.ViewModels {
public class ChatViewModel {
public string Message { get; set; }
public string FromUserId { get; set; }
public string FromUserName { get; set; }
public string ToUserId { get; set; }
public string ToUserName { get; set; }
}
}

View File

@@ -1,5 +1,4 @@
namespace PodNoms.Api.Models.ViewModels
{
namespace PodNoms.Api.Models.ViewModels {
public class ProcessProgressEvent {
public double Percentage { get; set; }
public string TotalSize;

View File

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Storage;
using PodNoms.Api.Utils;

View File

@@ -19,7 +19,7 @@
<PackageReference Include="Hangfire" Version="1.6.17" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.5.2" />
<PackageReference Include="Lib.Net.Http.WebPush" Version="1.3.0" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-preview2-final" />
<PackageReference Include="Microsoft.AspNetCore.App" Version="2.1.0-rc1-final" />
<PackageReference Include="AutoMapper" Version="6.2.2" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="3.2.0" />
<PackageReference Include="Handlebars.NetStandard" Version="1.8.1" />
@@ -31,7 +31,7 @@
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.0-dev001179" />
<PackageReference Include="SQLitePCLRaw.bundle_green" Version="1.1.10-pre20180223200113" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="2.3.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0-preview2-26406-04" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.0-rc1" />
<PackageReference Include="WindowsAzure.Storage" Version="8.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -8,5 +8,6 @@ namespace PodNoms.Api.Services.Auth {
public long? FacebookId { get; set; }
public string PictureUrl { get; set; }
public string Slug { get; set; }
public string FullName { get => $"{FirstName} {LastName}"; }
}
}

View File

@@ -1,12 +1,9 @@
using System;
using System.Security.Claims;
namespace PodNoms.Api.Services.Auth
{
public static class ClaimsPrincipalExtensions
{
public static string GetUserId(this ClaimsPrincipal principal)
{
namespace PodNoms.Api.Services.Auth {
public static class ClaimsPrincipalExtensions {
public static string GetUserId(this ClaimsPrincipal principal) {
if (principal == null)
throw new ArgumentNullException(nameof(principal));

View File

@@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
using PodNoms.Api.Services.Gravatar;
using PodNoms.Api.Models;
using PodNoms.Api.Utils;
using PodNoms.Api.Models.Settings;
namespace PodNoms.Api.Services.Auth {
public class PodNomsUserManager : UserManager<ApplicationUser> {

View File

@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
@@ -7,15 +8,12 @@ namespace PodNoms.Api.Services.Hubs {
[Authorize]
public class ChatHub : Hub {
public override async Task OnConnectedAsync() {
await Clients.All.SendAsync("SendAction", Context.User.Identity.Name, "joined");
// await Clients.All.SendAsync("SendAction", Context.User.Identity.Name, "joined");
}
public override async Task OnDisconnectedAsync(Exception ex) {
await Clients.All.SendAsync("SendAction", Context.User.Identity.Name, "left");
// await Clients.All.SendAsync("SendAction", Context.User.Identity.Name, "left");
}
public async Task Send(string message) {
await Clients.All.SendAsync("SendMessage", Context.User.Identity.Name, message);
}
}
}

View File

@@ -0,0 +1,8 @@
using System.Threading.Tasks;
using PodNoms.Api.Models.ViewModels;
namespace PodNoms.Api.Services {
public interface ISupportChatService {
Task<bool> InitiateSupportRequest(string fromUser, ChatViewModel message);
}
}

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.Options;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Persistence;
namespace PodNoms.Api.Services.Jobs {

View File

@@ -12,7 +12,6 @@ namespace PodNoms.Api.Services.Jobs {
IPushNotificationService notificationService) {
this._notificationService = notificationService;
this._subscriptionStore = subscriptionStore;
}
public async Task NotifyUser(string userId, string title, string body, string image) {
WebPush.PushMessage pushMessage = new WebPush.PushMessage(body) {

View File

@@ -4,6 +4,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Downloader;
using PodNoms.Api.Services.Processor;
@@ -16,12 +17,12 @@ namespace PodNoms.Api.Services.Jobs {
private readonly IAudioUploadProcessService _uploadService;
private readonly IConfiguration _options;
private readonly IPodcastRepository _podcastRepository;
private readonly ApplicationsSettings _applicationsSettings;
private readonly HelpersSettings _helpersSettings;
private readonly ILogger<ProcessPlaylistItemJob> _logger;
private readonly IUnitOfWork _unitOfWork;
public ProcessPlaylistItemJob(IPlaylistRepository playlistRepository, IEntryRepository entryRepository,
IAudioUploadProcessService uploadService, IConfiguration options,
IPodcastRepository podcastRepository, IOptions<ApplicationsSettings> applicationsSettings,
IPodcastRepository podcastRepository, IOptions<HelpersSettings> _helpersSettings,
IUnitOfWork unitOfWork, ILogger<ProcessPlaylistItemJob> logger) {
this._unitOfWork = unitOfWork;
this._playlistRepository = playlistRepository;
@@ -29,7 +30,7 @@ namespace PodNoms.Api.Services.Jobs {
this._uploadService = uploadService;
this._options = options;
this._podcastRepository = podcastRepository;
this._applicationsSettings = applicationsSettings.Value;
this._helpersSettings = _helpersSettings.Value;
this._logger = logger;
}
public async Task Execute() {
@@ -42,7 +43,7 @@ namespace PodNoms.Api.Services.Jobs {
var item = await _playlistRepository.GetParsedItem(itemId, playlistId);
if (item != null && !string.IsNullOrEmpty(item.VideoType) && item.VideoType.Equals("youtube")) {
var url = $"https://www.youtube.com/watch?v={item.VideoId}";
var downloader = new AudioDownloader(url, _applicationsSettings.Downloader);
var downloader = new AudioDownloader(url, _helpersSettings.Downloader);
var info = downloader.GetInfo();
if (info == AudioType.Valid) {
var podcast = await _podcastRepository.GetAsync(item.Playlist.PodcastId);

View File

@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NYoutubeDL.Models;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Downloader;
using PodNoms.Api.Utils.RemoteParsers;
@@ -17,21 +18,21 @@ namespace PodNoms.Api.Services.Jobs {
public class ProcessPlaylistsJob : IJob {
public readonly IPlaylistRepository _playlistRepository;
public readonly IEntryRepository _entryRepository;
private readonly ApplicationsSettings _applicationsSettings;
private readonly HelpersSettings _helpersSettings;
private readonly ILogger<ProcessPlaylistsJob> _logger;
private readonly YouTubeParser _youTubeParser;
private readonly MixcloudParser _mixcloudParser;
private readonly IUnitOfWork _unitOfWork;
public ProcessPlaylistsJob(IPlaylistRepository playlistRepository, IEntryRepository entryRepository,
IUnitOfWork unitOfWork, IOptions<ApplicationsSettings> applicationsSettings,
IUnitOfWork unitOfWork, IOptions<HelpersSettings> helpersSettings,
ILoggerFactory logger, YouTubeParser youTubeParser, MixcloudParser mixcloudParser) {
this._unitOfWork = unitOfWork;
this._youTubeParser = youTubeParser;
this._mixcloudParser = mixcloudParser;
this._playlistRepository = playlistRepository;
this._entryRepository = entryRepository;
this._applicationsSettings = applicationsSettings.Value;
this._helpersSettings = helpersSettings.Value;
this._logger = logger.CreateLogger<ProcessPlaylistsJob>();
}
@@ -40,7 +41,7 @@ namespace PodNoms.Api.Services.Jobs {
var resultList = new List<ParsedItemResult>();
foreach (var playlist in playlists) {
var downloader = new AudioDownloader(playlist.SourceUrl, _applicationsSettings.Downloader);
var downloader = new AudioDownloader(playlist.SourceUrl, _helpersSettings.Downloader);
var info = downloader.GetInfo();
var id = ((PlaylistDownloadInfo)downloader.RawProperties).Id;
if (info == AudioType.Playlist && downloader.RawProperties is PlaylistDownloadInfo) {

View File

@@ -10,6 +10,7 @@ using PodNoms.Api.Models;
using System.Net;
using PodNoms.Api.Utils;
using HandlebarsDotNet;
using PodNoms.Api.Models.Settings;
namespace PodNoms.Api.Services {
public class MailgunSender : IMailSender {

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Realtime;

View File

@@ -11,6 +11,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Persistence;
using PodNoms.Api.Services.Downloader;
@@ -23,14 +24,14 @@ namespace PodNoms.Api.Services.Processor {
private readonly IUnitOfWork _unitOfWork;
private readonly IEntryRepository _repository;
public ApplicationsSettings _applicationsSettings { get; }
public HelpersSettings _helpersSettings { get; }
private readonly HubLifetimeManager<UserUpdatesHub> _userUpdateHub;
public UrlProcessService(IEntryRepository repository, IUnitOfWork unitOfWork,
IFileUploader fileUploader, IOptions<ApplicationsSettings> applicationsSettings,
IFileUploader fileUploader, IOptions<HelpersSettings> helpersSettings,
HubLifetimeManager<UserUpdatesHub> userUpdateHub,
ILoggerFactory logger, IMapper mapper, IRealTimeUpdater pusher) : base(logger, mapper, pusher) {
this._applicationsSettings = applicationsSettings.Value;
this._helpersSettings = helpersSettings.Value;
this._repository = repository;
this._unitOfWork = unitOfWork;
this._userUpdateHub = userUpdateHub;
@@ -56,7 +57,7 @@ namespace PodNoms.Api.Services.Processor {
public async Task<AudioType> GetInformation(PodcastEntry entry) {
var downloader = new AudioDownloader(entry.SourceUrl, _applicationsSettings.Downloader);
var downloader = new AudioDownloader(entry.SourceUrl, _helpersSettings.Downloader);
var ret = downloader.GetInfo();
if (ret == AudioType.Valid) {
entry.Title = downloader.Properties?.Title;
@@ -81,7 +82,7 @@ namespace PodNoms.Api.Services.Processor {
if (entry == null)
return false;
try {
var downloader = new AudioDownloader(entry.SourceUrl, _applicationsSettings.Downloader);
var downloader = new AudioDownloader(entry.SourceUrl, _helpersSettings.Downloader);
var outputFile =
Path.Combine(System.IO.Path.GetTempPath(), $"{System.Guid.NewGuid().ToString()}.mp3");

View File

@@ -13,10 +13,10 @@ namespace PodNoms.Api.Services.Push {
private readonly ILogger<PushServicePushNotificationService> _logger;
private readonly IHttpClientFactory _httpClientFactory;
public string PublicKey => _options.PublicKey;
public FirebasePushNotificationService(IOptions<PushNotificationServiceOptions> optionsAccessor,
public FirebasePushNotificationService(IOptions<PushNotificationServiceOptions> pushOptions,
IHttpClientFactory httpClientFactory,
ILogger<PushServicePushNotificationService> logger) {
_options = optionsAccessor.Value;
_options = pushOptions.Value;
_logger = logger;
_httpClientFactory = httpClientFactory;
}

View File

@@ -9,6 +9,7 @@ using Microsoft.Extensions.Options;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Services.Processor;
using PodNoms.Api.Services.Realtime;
using PodNoms.Api.Utils.Extensions;

View File

@@ -0,0 +1,55 @@
using System.Threading.Tasks;
using Lib.Net.Http.WebPush;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Services.Auth;
using PodNoms.Api.Services.Hubs;
using PodNoms.Api.Services.Push;
using WebPush = Lib.Net.Http.WebPush;
namespace PodNoms.Api.Services {
public class SupportChatService : ISupportChatService {
private readonly ChatSettings _chatSettings;
private readonly IPushNotificationService _notificationService;
private readonly HubLifetimeManager<ChatHub> _chatHub;
private readonly UserManager<ApplicationUser> _userManager;
private readonly IPushSubscriptionStore _subscriptionStore;
private readonly HubLifetimeManager<ChatHub> _hub;
public SupportChatService(UserManager<ApplicationUser> userManager, IOptions<ChatSettings> chatSettings,
IPushSubscriptionStore subscriptionStore, IPushNotificationService notificationService,
HubLifetimeManager<ChatHub> chatHub) {
this._chatSettings = chatSettings.Value;
this._notificationService = notificationService;
this._chatHub = chatHub;
this._userManager = userManager;
this._subscriptionStore = subscriptionStore;
}
public async Task<bool> InitiateSupportRequest(string fromUser, ChatViewModel message) {
if (!string.IsNullOrEmpty(_chatSettings.CurrentChatUser)) {
var user = await _userManager.FindByEmailAsync(_chatSettings.CurrentChatUser);
if (!string.IsNullOrEmpty(user?.Id)) {
message.ToUserId = user.Id;
message.ToUserName = user.FullName;
//send firebase message to notify via web worker
WebPush.PushMessage pushMessage = new WebPush.PushMessage(message.Message) {
Topic = "New support chat message",
Urgency = PushMessageUrgency.Normal
};
await _subscriptionStore.ForEachSubscriptionAsync(user.Id, (WebPush.PushSubscription subscription) => {
_notificationService.SendNotificationAsync(subscription, pushMessage);
});
//send SignalR message to notify in chat.component
await _chatHub.SendUserAsync(user.Email, "SendMessage", new object[] { message });
return true;
}
}
return false;
}
}
}

View File

@@ -26,6 +26,7 @@ using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
using PodNoms.Api.Models.ViewModels;
using PodNoms.Api.Persistence;
using PodNoms.Api.Providers;
@@ -97,9 +98,10 @@ namespace PodNoms.Api {
services.AddOptions();
services.Configure<AppSettings>(Configuration.GetSection("App"));
services.Configure<StorageSettings>(Configuration.GetSection("Storage"));
services.Configure<ApplicationsSettings>(Configuration.GetSection("ApplicationsSettings"));
services.Configure<HelpersSettings>(Configuration.GetSection("HelpersSettings"));
services.Configure<EmailSettings>(Configuration.GetSection("EmailSettings"));
services.Configure<FacebookAuthSettings>(Configuration.GetSection("FacebookAuthSettings"));
services.Configure<ChatSettings>(Configuration.GetSection("ChatSettings"));
services.Configure<ImageFileStorageSettings>(Configuration.GetSection("ImageFileStorageSettings"));
services.Configure<AudioFileStorageSettings>(Configuration.GetSection("AudioFileStorageSettings"));
services.Configure<FormOptions>(options => {
@@ -188,7 +190,7 @@ namespace PodNoms.Api {
.AddJsonOptions(options => {
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize;
})
})
.AddXmlSerializerFormatters()
.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
@@ -201,10 +203,24 @@ namespace PodNoms.Api {
x.ValueLengthLimit = int.MaxValue;
x.MultipartBodyLengthLimit = int.MaxValue; // In case of multipart
});
services.AddSignalR(config => { });
services.AddSignalR()
.AddJsonProtocol(options => options.PayloadSerializerSettings.ContractResolver = new DefaultContractResolver() {
NamingStrategy = new CamelCaseNamingStrategy() {
ProcessDictionaryKeys = true
}
});
services.AddCors(options => {
options.AddPolicy("AllowAllOrigins",
options.AddPolicy("PodNomsClientPolicy",
builder => builder
.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins("http://localhost:4200", "https://*.podnoms.com")
.AllowCredentials());
});
services.AddCors(options => {
options.AddPolicy("AllowAllPolicy",
builder => builder
.AllowAnyOrigin()
.AllowAnyMethod()
@@ -224,6 +240,7 @@ namespace PodNoms.Api {
services.AddScoped<IUrlProcessService, UrlProcessService>();
services.AddScoped<INotifyJobCompleteService, NotifyJobCompleteService>();
services.AddScoped<IAudioUploadProcessService, AudioUploadProcessService>();
services.AddScoped<ISupportChatService, SupportChatService>();
services.AddScoped<IMailSender, MailgunSender>();
services.AddScoped<YouTubeParser>();
services.AddScoped<MixcloudParser>();
@@ -271,7 +288,7 @@ namespace PodNoms.Api {
});
app.UseAuthentication();
app.UseCors("AllowAllOrigins");
app.UseCors("AllowAllPolicy");
app.UseSignalR(routes => {
routes.MapHub<AudioProcessingHub>("/hubs/audioprocessing");

View File

@@ -6,6 +6,7 @@ using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Microsoft.Extensions.Options;
using PodNoms.Api.Models;
using PodNoms.Api.Models.Settings;
namespace PodNoms.Api.Utils.RemoteParsers {
public partial class YouTubeParser {

View File

@@ -23,7 +23,7 @@
"Key": "9f59ab0666214980ef76",
"Cluster": "eu"
},
"ApplicationsSettings": {
"HelpersSettings": {
"Downloader": "/usr/local/bin/youtube-dl"
},
"ImageFileStorageSettings": {