Browse Source

Authenticate with twitter.

pull/314/head
Sebastian Stehle 7 years ago
parent
commit
0c3d9dcb59
  1. 4
      src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs
  2. 28
      src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs
  3. 9
      src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs
  4. 10
      src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs
  5. 42
      src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs
  6. 10
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  7. 38
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html
  8. 64
      src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts
  9. 19
      src/Squidex/app/theme/_bootstrap.scss
  10. 2
      src/Squidex/app/theme/_vars.scss
  11. 4
      src/Squidex/appsettings.json
  12. 24
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/TweetActionTests.cs

4
src/Squidex.Domain.Apps.Core.Model/Rules/Actions/TweetAction.cs

@ -12,7 +12,9 @@ namespace Squidex.Domain.Apps.Core.Rules.Actions
[TypeName(nameof(TweetAction))] [TypeName(nameof(TweetAction))]
public sealed class TweetAction : RuleAction public sealed class TweetAction : RuleAction
{ {
public string PinCode { get; set; } public string AccessToken { get; set; }
public string AccessSecret { get; set; }
public string Text { get; set; } public string Text { get; set; }

28
src/Squidex.Domain.Apps.Core.Operations/HandleRules/Actions/TweetActionHandler.cs

@ -19,7 +19,9 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
public sealed class TweetJob public sealed class TweetJob
{ {
public string PinCode { get; set; } public string AccessToken { get; set; }
public string AccessSecret { get; set; }
public string Text { get; set; } public string Text { get; set; }
} }
@ -29,7 +31,7 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
private const string Description = "Send a tweet"; private const string Description = "Send a tweet";
private readonly RuleEventFormatter formatter; private readonly RuleEventFormatter formatter;
private readonly ClientPool<string, Tokens> tokenPool; private readonly TwitterOptions twitterOptions;
public TweetActionHandler(RuleEventFormatter formatter, IOptions<TwitterOptions> twitterOptions) public TweetActionHandler(RuleEventFormatter formatter, IOptions<TwitterOptions> twitterOptions)
{ {
@ -38,19 +40,19 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
this.formatter = formatter; this.formatter = formatter;
tokenPool = new ClientPool<string, Tokens>(async key => this.twitterOptions = twitterOptions.Value;
{
var session = await OAuth.AuthorizeAsync(twitterOptions.Value.ClientId, twitterOptions.Value.ClientSecret);
return await session.GetTokensAsync(key);
});
} }
protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action) protected override (string Description, TweetJob Data) CreateJob(EnrichedEvent @event, TweetAction action)
{ {
var text = formatter.Format(action.Text, @event); var text = formatter.Format(action.Text, @event);
var ruleJob = new TweetJob { Text = text, PinCode = action.PinCode }; var ruleJob = new TweetJob
{
Text = text,
AccessToken = action.AccessToken,
AccessSecret = action.AccessSecret
};
return (Description, ruleJob); return (Description, ruleJob);
} }
@ -59,9 +61,13 @@ namespace Squidex.Domain.Apps.Core.HandleRules.Actions
{ {
try try
{ {
var tokens = await tokenPool.GetClientAsync(job.PinCode); var tokens = Tokens.Create(
twitterOptions.ClientId,
twitterOptions.ClientSecret,
job.AccessToken,
job.AccessSecret);
var response = await tokens.Statuses.UpdateAsync(x => job.Text); var response = await tokens.Statuses.UpdateAsync(status => job.Text);
return ($"Tweeted: {job.Text}", null); return ($"Tweeted: {job.Text}", null);
} }

9
src/Squidex.Domain.Apps.Entities/Rules/Guards/RuleActionValidator.cs

@ -133,9 +133,14 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards
{ {
var errors = new List<ValidationError>(); var errors = new List<ValidationError>();
if (string.IsNullOrWhiteSpace(action.PinCode)) if (string.IsNullOrWhiteSpace(action.AccessToken))
{
errors.Add(new ValidationError("Access Tokenis required.", nameof(action.AccessToken)));
}
if (string.IsNullOrWhiteSpace(action.AccessToken))
{ {
errors.Add(new ValidationError("Pin Code is required.", nameof(action.PinCode))); errors.Add(new ValidationError("Access Secret is required.", nameof(action.AccessSecret)));
} }
return Task.FromResult<IEnumerable<ValidationError>>(errors); return Task.FromResult<IEnumerable<ValidationError>>(errors);

10
src/Squidex/Areas/Api/Controllers/Rules/Models/Actions/TweetActionDto.cs

@ -17,10 +17,16 @@ namespace Squidex.Areas.Api.Controllers.Rules.Models.Actions
public sealed class TweetActionDto : RuleActionDto public sealed class TweetActionDto : RuleActionDto
{ {
/// <summary> /// <summary>
/// The pin code. /// The access token.
/// </summary> /// </summary>
[Required] [Required]
public string PinCode { get; set; } public string AccessToken { get; set; }
/// <summary>
/// The access secret.
/// </summary>
[Required]
public string AccessSecret { get; set; }
/// <summary> /// <summary>
/// The text that is sent as tweet to twitter. /// The text that is sent as tweet to twitter.

42
src/Squidex/Areas/Api/Controllers/Rules/TwitterController.cs

@ -6,10 +6,10 @@
// ========================================================================== // ==========================================================================
using System.Threading.Tasks; using System.Threading.Tasks;
using CoreTweet;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Squidex.Domain.Apps.Core.Rules.Actions; using Squidex.Domain.Apps.Core.Rules.Actions;
using static CoreTweet.OAuth;
namespace Squidex.Areas.Api.Controllers.Rules namespace Squidex.Areas.Api.Controllers.Rules
{ {
@ -22,12 +22,48 @@ namespace Squidex.Areas.Api.Controllers.Rules
this.twitterOptions = twitterOptions.Value; this.twitterOptions = twitterOptions.Value;
} }
public sealed class TokenRequest
{
public string PinCode { get; set; }
public string RequestToken { get; set; }
public string RequestTokenSecret { get; set; }
}
[HttpGet]
[Route("rules/twitter/auth")] [Route("rules/twitter/auth")]
public async Task<IActionResult> Auth() public async Task<IActionResult> Auth()
{ {
var session = await OAuth.AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret); var session = await AuthorizeAsync(twitterOptions.ClientId, twitterOptions.ClientSecret);
return Redirect(session.AuthorizeUri.ToString()); return Ok(new
{
session.AuthorizeUri,
session.RequestToken,
session.RequestTokenSecret
});
}
[HttpPost]
[Route("rules/twitter/token")]
public async Task<IActionResult> AuthComplete([FromBody] TokenRequest request)
{
var session = new OAuthSession
{
ConsumerKey = twitterOptions.ClientId,
ConsumerSecret = twitterOptions.ClientSecret,
RequestToken = request.RequestToken,
RequestTokenSecret = request.RequestTokenSecret
};
var tokens = await session.GetTokensAsync(request.PinCode);
return Ok(new
{
tokens.AccessToken,
tokens.AccessTokenSecret
});
} }
} }
} }

10
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -8,11 +8,11 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using NSwag.Annotations; using NSwag.Annotations;
using Orleans; using Orleans;
using Squidex.Areas.Api.Controllers.UI.Models; using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Config; using Squidex.Config;
using Squidex.Domain.Apps.Core.Rules.Actions;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Pipeline; using Squidex.Pipeline;
@ -29,13 +29,18 @@ namespace Squidex.Areas.Api.Controllers.UI
public sealed class UIController : ApiController public sealed class UIController : ApiController
{ {
private readonly MyUIOptions uiOptions; private readonly MyUIOptions uiOptions;
private readonly TwitterOptions twitterOptions;
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
public UIController(ICommandBus commandBus, IOptions<MyUIOptions> uiOptions, IGrainFactory grainFactory) public UIController(ICommandBus commandBus,
IOptions<MyUIOptions> uiOptions,
IOptions<TwitterOptions> twitterOptions,
IGrainFactory grainFactory)
: base(commandBus) : base(commandBus)
{ {
this.uiOptions = uiOptions.Value; this.uiOptions = uiOptions.Value;
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
this.twitterOptions = twitterOptions.Value;
} }
/// <summary> /// <summary>
@ -56,6 +61,7 @@ namespace Squidex.Areas.Api.Controllers.UI
result.Value["mapType"] = uiOptions.Map?.Type ?? "OSM"; result.Value["mapType"] = uiOptions.Map?.Type ?? "OSM";
result.Value["mapKey"] = uiOptions.Map?.GoogleMaps?.Key; result.Value["mapKey"] = uiOptions.Map?.GoogleMaps?.Key;
result.Value["supportTwitterAction"] = twitterOptions.IsConfigured();
return Ok(result.Value); return Ok(result.Value);
} }

38
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.html

@ -2,16 +2,42 @@
<form [formGroup]="actionForm" class="form-horizontal"> <form [formGroup]="actionForm" class="form-horizontal">
<div class="form-group row"> <div class="form-group row">
<label class="col col-3 col-form-label" for="pinCode">Pin Code</label> <div class="col col-9 offset-3">
<ng-container *ngIf="!isRedirected">
<button class="btn btn-twitter" [disabled]="isAuthenticating" (click)="auth()">
Request access token with twitter
</button>
</ng-container>
<ng-container *ngIf="isRedirected">
<form class="form-inline" (ngSubmit)="complete()">
<input class="form-control mr-1" [(ngModel)]="pinCode" [ngModelOptions]="{ standalone: true }" placeholder="Pin" />
<button type="submit" class="btn btn-secondary" [disabled]="!pinCode">
Complete
</button>
</form>
</ng-container>
</div>
</div>
<div class="form-group row">
<label class="col col-3 col-form-label" for="accessToken">Access Token</label>
<div class="col col-9"> <div class="col col-9">
<sqx-control-errors for="pinCode" [submitted]="actionFormSubmitted"></sqx-control-errors> <sqx-control-errors for="accessToken" submitOnly="true" [submitted]="actionFormSubmitted"></sqx-control-errors>
<input type="text" readonly class="form-control" id="accessToken" formControlName="accessToken" />
</div>
</div>
<input type="pinCode" class="form-control" id="pinCode" formControlName="pinCode" /> <div class="form-group row">
<label class="col col-3 col-form-label" for="accessToken">Access Secret</label>
<small class="form-text text-muted"> <div class="col col-9">
The pin code to authorize. Follow this link and add the pin code: <a href="/api/rules/twitter/auth" target="_blank">Login to twitter</a>. <sqx-control-errors for="accessSecret" submitOnly="true" [submitted]="actionFormSubmitted"></sqx-control-errors>
</small>
<input type="text" readonly class="form-control" id="accessSecret" formControlName="accessSecret" />
</div> </div>
</div> </div>

64
src/Squidex/app/features/rules/pages/rules/actions/tweet-action.component.ts

@ -5,15 +5,20 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { HttpClient } from '@angular/common/http';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms'; import { FormControl, FormGroup, Validators } from '@angular/forms';
import { DialogService } from '@app/shared';
@Component({ @Component({
selector: 'sqx-tweet-action', selector: 'sqx-tweet-action',
styleUrls: ['./tweet-action.component.scss'], styleUrls: ['./tweet-action.component.scss'],
templateUrl: './tweet-action.component.html' templateUrl: './tweet-action.component.html'
}) })
export class TweetActionComponent implements OnInit { export class TweetActionComponent implements OnInit {
private request: any;
@Input() @Input()
public action: any; public action: any;
@ -23,9 +28,25 @@ export class TweetActionComponent implements OnInit {
@Input() @Input()
public actionFormSubmitted = false; public actionFormSubmitted = false;
public isAuthenticating = false;
public isRedirected = false;
public pinCode: string;
constructor(
private readonly dialogs: DialogService,
private readonly httpClient: HttpClient
) {
}
public ngOnInit() { public ngOnInit() {
this.actionForm.setControl('pinCode', this.actionForm.setControl('accessToken',
new FormControl(this.action.pinCode || '', [ new FormControl(this.action.accessToken || '', [
Validators.required
]));
this.actionForm.setControl('accessSecret',
new FormControl(this.action.accessSecret || '', [
Validators.required Validators.required
])); ]));
@ -34,4 +55,43 @@ export class TweetActionComponent implements OnInit {
Validators.required Validators.required
])); ]));
} }
public auth() {
this.isAuthenticating = true;
this.httpClient.get('api/rules/twitter/auth')
.subscribe((response: any) => {
this.request = {
requestToken: response.requestToken,
requestTokenSecret: response.requestTokenSecret
};
this.isAuthenticating = false;
this.isRedirected = true;
window.open(response.authorizeUri, '_blank');
}, () => {
this.dialogs.notifyError('Failed to authenticate with twitter.');
this.isAuthenticating = false;
this.isRedirected = false;
});
}
public complete() {
this.request.pinCode = this.pinCode;
this.httpClient.post('api/rules/twitter/token', this.request)
.subscribe((response: any) => {
this.actionForm.get('accessToken')!.setValue(response.accessToken);
this.actionForm.get('accessSecret')!.setValue(response.accessTokenSecret);
this.isRedirected = false;
}, () => {
this.dialogs.notifyError('Failed to request access token.');
this.isAuthenticating = false;
this.isRedirected = false;
});
}
} }

19
src/Squidex/app/theme/_bootstrap.scss

@ -264,6 +264,10 @@ a {
&-microsoft { &-microsoft {
color: $color-extern-microsoft-icon; color: $color-extern-microsoft-icon;
} }
&-twitter {
color: $color-extern-twitter-icon;
}
} }
// //
@ -304,6 +308,21 @@ a {
} }
} }
&-twitter {
@include button-variant($color-extern-twitter, $color-extern-twitter);
& {
color: $color-dark-foreground;
}
&:hover,
&:focus {
.icon-twitter {
color: darken($color-extern-twitter-icon, 5%);
}
}
}
// Special radio button. // Special radio button.
&-radio { &-radio {
& { & {

2
src/Squidex/app/theme/_vars.scss

@ -19,6 +19,8 @@ $color-extern-microsoft: #004185;
$color-extern-microsoft-icon: #1b67b7; $color-extern-microsoft-icon: #1b67b7;
$color-extern-github: #191919; $color-extern-github: #191919;
$color-extern-github-icon: #4a4a4a; $color-extern-github-icon: #4a4a4a;
$color-extern-twitter: #1da1f2;
$color-extern-twitter-icon: #1681bf;
$color-theme-blue: #3389ff; $color-theme-blue: #3389ff;
$color-theme-blue-dark: #297ff6; $color-theme-blue-dark: #297ff6;

4
src/Squidex/appsettings.json

@ -253,10 +253,10 @@
/* /*
* The client id for twitter. * The client id for twitter.
*/ */
"clientId": "", "clientId": "QZhb3HQcGCvE6G8yNNP9ksNet",
/* /*
* The client secret for twitter. * The client secret for twitter.
*/ */
"clientSecret": "" "clientSecret": "Pdu9wdN72T33KJRFdFy1w4urBKDRzIyuKpc0OItQC2E616DuZD"
} }
} }

24
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Actions/TweetActionTests.cs

@ -17,23 +17,37 @@ namespace Squidex.Domain.Apps.Entities.Rules.Guards.Actions
public class TweetActionTests public class TweetActionTests
{ {
[Fact] [Fact]
public async Task Should_add_error_if_pin_code_is_null() public async Task Should_add_error_if_access_token_is_null()
{ {
var action = new TweetAction { PinCode = null }; var action = new TweetAction { AccessToken = null, AccessSecret = "secret" };
var errors = await RuleActionValidator.ValidateAsync(action); var errors = await RuleActionValidator.ValidateAsync(action);
errors.Should().BeEquivalentTo( errors.Should().BeEquivalentTo(
new List<ValidationError> new List<ValidationError>
{ {
new ValidationError("Pin Code is required.", "PinCode") new ValidationError("Access Token is required.", "AccessToken")
}); });
} }
[Fact] [Fact]
public async Task Should_not_add_error_if_pin_code_is_absolute() public async Task Should_add_error_if_access_secret_is_null()
{ {
var action = new TweetAction { PinCode = "123" }; var action = new TweetAction { AccessToken = "token", AccessSecret = null };
var errors = await RuleActionValidator.ValidateAsync(action);
errors.Should().BeEquivalentTo(
new List<ValidationError>
{
new ValidationError("Access Secret is required.", "AccessSecret")
});
}
[Fact]
public async Task Should_not_add_error_if_access_token_and_secret_defined()
{
var action = new TweetAction { AccessToken = "token", AccessSecret = "secret" };
var errors = await RuleActionValidator.ValidateAsync(action); var errors = await RuleActionValidator.ValidateAsync(action);

Loading…
Cancel
Save