Improved inital setup by adding a short wizard.

This commit is contained in:
Roger Far 2021-01-09 12:34:05 -07:00
parent df39d6a671
commit ec02b3d8bb
19 changed files with 345 additions and 159 deletions

View file

@ -1,22 +1,30 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { MainLayoutComponent } from './main-layout/main-layout.component';
import { RouterModule, Routes } from '@angular/router';
import { AuthResolverService } from './auth-resolver.service';
import { LoginComponent } from './login/login.component';
import { MainLayoutComponent } from './main-layout/main-layout.component';
import { SetupComponent } from './setup/setup.component';
const routes: Routes = [
{
path: 'login',
component: LoginComponent,
},
{
path: 'setup',
component: SetupComponent,
},
{
path: '',
component: MainLayoutComponent,
canActivate: [],
resolve: {
isLoggedIn: AuthResolverService,
},
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, { relativeLinkResolution: 'legacy' })],
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}

View file

@ -18,6 +18,7 @@ import { FileStatusPipe } from './file-status.pipe';
import { LoginComponent } from './login/login.component';
import { AuthInterceptor } from './auth.interceptor';
import { curray } from 'curray';
import { SetupComponent } from './setup/setup.component';
curray();
@ -34,6 +35,7 @@ curray();
TorrentStatusPipe,
FileStatusPipe,
LoginComponent,
SetupComponent,
],
imports: [
BrowserModule,

View file

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
@Injectable({
providedIn: 'root',
})
export class AuthResolverService implements Resolve<Observable<any>> {
constructor(private authService: AuthService) {}
resolve() {
return this.authService.isLoggedIn();
}
}

View file

@ -3,7 +3,7 @@ import {
HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpRequest
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
@ -20,7 +20,9 @@ export class AuthInterceptor implements HttpInterceptor {
): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error && (error.status === 401 || error.status === 403)) {
if (error && error.status === 402) {
this.router.navigate(['/setup']);
} else if (error && (error.status === 401 || error.status === 403)) {
this.router.navigate(['/login']);
}
return throwError(error);

View file

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
@Injectable({
@ -8,6 +8,17 @@ import { Observable } from 'rxjs/internal/Observable';
export class AuthService {
constructor(private http: HttpClient) {}
public isLoggedIn(): Observable<void> {
return this.http.get<void>(`/Api/Authentication/IsLoggedIn`);
}
public create(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Create`, {
userName,
password,
});
}
public login(userName: string, password: string): Observable<void> {
return this.http.post<void>(`/Api/Authentication/Login`, {
userName,

View file

@ -2,7 +2,7 @@
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-4-desktop is-3-widescreen">
<div class="column is-5-tablet is-5-desktop is-5-widescreen">
<img src="../../assets/logo.png" />
<div class="box">
<div class="field">
@ -41,6 +41,7 @@
class="button is-success"
(click)="login()"
[ngClass]="{ 'is-loading': loggingIn }"
[disabled]="loggingIn"
>
Login
</button>

View file

@ -36,7 +36,7 @@
[(ngModel)]="settingDownloadFolder"
/>
</div>
<p class="help">Path on the host where Real Debrid Client is hosted.</p>
<p class="help">Path on the host where RealDebrid Client is hosted.</p>
</div>
<div class="field">
<label class="label">Maximum parallel downloads</label>

View file

@ -0,0 +1,143 @@
<section class="hero is-dark is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns is-centered">
<div class="column is-5-tablet is-5-desktop is-5-widescreen">
<img src="../../assets/logo.png" />
<div class="box" *ngIf="step === 1">
<div class="notification is-primary">
Welcome to RealDebrid Client. Please create your account by
entering a username and password.
</div>
<div class="field">
<label for="" class="label">Username</label>
<div class="control has-icons-left">
<input
type="text"
placeholder=""
class="input"
name="userName"
[(ngModel)]="userName"
required
/>
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
</div>
</div>
<div class="field">
<label for="" class="label" name="password">Password</label>
<div class="control has-icons-left">
<input
type="password"
class="input"
required
name="password"
[(ngModel)]="password"
/>
<span class="icon is-small is-left">
<i class="fa fa-lock"></i>
</span>
</div>
</div>
<div class="field">
<button
class="button is-success"
(click)="setup()"
[ngClass]="{ 'is-loading': working }"
[disabled]="working"
>
Setup
</button>
</div>
<div
class="notification is-danger is-light"
*ngIf="error?.length > 0"
>
{{ error }}
</div>
</div>
<div class="box" *ngIf="step === 2">
<div class="notification is-primary">
To be able to use the RealDebrid Client you need a premium
subscription to download torrents.
<br /><br />
Not premium yet?
<a
href="https://real-debrid.com/?id=1348683"
target="_blank"
rel="noopener"
>Use this link to sign up to RealDebrid.</a
>
<br /><br />
To connect to RealDebrid please enter your Personal Prive API
Token found here:
<a
href="https://real-debrid.com/apitoken"
target="_blank"
rel="noopener"
>https://real-debrid.com/apitoken</a
>.
</div>
<div class="field">
<label for="" class="label">API Private Token</label>
<div class="control has-icons-left">
<input
type="text"
placeholder=""
class="input"
name="token"
[(ngModel)]="token"
required
/>
<span class="icon is-small is-left">
<i class="fa fa-envelope"></i>
</span>
</div>
</div>
<div class="field">
<button
class="button is-success"
(click)="setToken()"
[ngClass]="{ 'is-loading': working }"
[disabled]="working"
>
Setup
</button>
</div>
<div
class="notification is-danger is-light"
*ngIf="error?.length > 0"
>
{{ error }}
</div>
</div>
<div class="box" *ngIf="step === 3">
<div class="notification is-primary">
You are now connected to RealDebrid! To setup Radarr or Sonarr,
please following the instructions here:
<a
href="https://github.com/rogerfar/rdt-client/"
target="_blank"
rel="noopener"
>https://github.com/rogerfar/rdt-client/</a
>.
</div>
<div class="field">
<button
class="button is-success"
(click)="close()"
[ngClass]="{ 'is-loading': working }"
[disabled]="working"
>
OK
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,66 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from '../auth.service';
import { Setting } from '../models/setting.model';
import { SettingsService } from '../settings.service';
@Component({
selector: 'app-setup',
templateUrl: './setup.component.html',
styleUrls: ['./setup.component.scss'],
})
export class SetupComponent implements OnInit {
public userName: string;
public password: string;
public token: string;
public error: string;
public working: boolean;
public step: number = 1;
constructor(
private authService: AuthService,
private settingsService: SettingsService,
private router: Router
) {}
ngOnInit(): void {}
public setup(): void {
this.error = null;
this.working = true;
this.authService.create(this.userName, this.password).subscribe(
() => {
this.step = 2;
this.working = false;
},
(err) => {
this.working = false;
this.error = err.error;
}
);
}
public setToken(): void {
const setting = new Setting();
setting.settingId = 'RealDebridApiKey';
setting.value = this.token;
this.settingsService.update([setting]).subscribe(
() => {
this.step = 3;
this.working = false;
},
(err) => {
this.working = false;
this.error = err.error;
}
);
}
public close(): void {
this.router.navigate(['/']);
}
}

View file

@ -1,4 +1,6 @@
using System.Linq;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using RdtClient.Data.Models.Data;
@ -33,5 +35,42 @@ namespace RdtClient.Data.Data
fk.DeleteBehavior = DeleteBehavior.Restrict;
}
}
public async Task Seed()
{
var seedSettings = new List<Setting>
{
new Setting
{
SettingId = "RealDebridApiKey",
Type = "String",
Value = ""
},
new Setting
{
SettingId = "DownloadFolder",
Type = "String",
Value = "/data/downloads"
},
new Setting
{
SettingId = "DownloadLimit",
Type = "Int32",
Value = "10"
}
};
var dbSettings = await Settings.ToListAsync();
foreach (var seedSetting in seedSettings)
{
var dbSetting = dbSettings.FirstOrDefault(m => m.SettingId == seedSetting.SettingId);
if (dbSetting == null)
{
await Settings.AddAsync(seedSetting);
await SaveChangesAsync();
}
}
}
}
}

View file

@ -1,63 +0,0 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using RdtClient.Data.Models.Data;
namespace RdtClient.Data.Data
{
public static class DataMigration
{
private static Boolean InDocker => Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER") == "true";
public static void Setup(IServiceScope serviceScope)
{
using var scope = serviceScope.ServiceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();
dataContext.Database.Migrate();
Seed(dataContext);
}
private static void Seed(DataContext dataContext)
{
var defaultDownloadPath = "/data/downloads";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !InDocker)
{
defaultDownloadPath = @"C:\Downloads";
}
var configuration = dataContext.Settings.FirstOrDefault();
if (configuration == null)
{
dataContext.Settings.Add(new Setting
{
SettingId = "RealDebridApiKey",
Type = "String",
Value = ""
});
dataContext.Settings.Add(new Setting
{
SettingId = "DownloadFolder",
Type = "String",
Value = defaultDownloadPath
});
dataContext.Settings.Add(new Setting
{
SettingId = "DownloadLimit",
Type = "Int32",
Value = "10"
});
dataContext.SaveChanges();
}
}
}
}

View file

@ -16,6 +16,50 @@ namespace RdtClient.Web.Controllers
{
_authentication = authentication;
}
[AllowAnonymous]
[Route("IsLoggedIn")]
[HttpGet]
public async Task<ActionResult> IsLoggedIn()
{
if (User.Identity?.IsAuthenticated == false)
{
var user = await _authentication.GetUser();
if (user == null)
{
return StatusCode(402, "Setup required");
}
return Unauthorized();
}
return Ok();
}
[AllowAnonymous]
[Route("Create")]
[HttpPost]
public async Task<ActionResult> Create([FromBody] AuthControllerLoginRequest request)
{
var user = await _authentication.GetUser();
if (user != null)
{
return StatusCode(401);
}
var registerResult = await _authentication.Register(request.UserName, request.Password);
if (!registerResult.Succeeded)
{
return BadRequest(registerResult.Errors.First().Description);
}
await _authentication.Login(request.UserName, request.Password);
return Ok();
}
[AllowAnonymous]
[Route("Login")]
@ -26,12 +70,7 @@ namespace RdtClient.Web.Controllers
if (user == null)
{
var registerResult = await _authentication.Register(request.UserName, request.Password);
if (!registerResult.Succeeded)
{
return BadRequest(registerResult.Errors.First().Description);
}
return StatusCode(402);
}
var result = await _authentication.Login(request.UserName, request.Password);

View file

@ -28,6 +28,7 @@ namespace RdtClient.Web
{
var dbContext = scope.ServiceProvider.GetRequiredService<DataContext>();
await dbContext.Database.MigrateAsync();
await dbContext.Seed();
}
await host.RunAsync();

View file

@ -6,24 +6,6 @@
<UserSecretsId>94c24cba-f03f-4453-a671-3640b517c573</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<None Remove="serviceinstall.bat" />
<None Remove="serviceremove.bat" />
<None Remove="startup.bat" />
</ItemGroup>
<ItemGroup>
<Content Include="serviceinstall.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="serviceremove.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="startup.bat">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.1" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="5.0.1" />

View file

@ -1,3 +1,4 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
@ -85,7 +86,7 @@ namespace RdtClient.Web
Service.DiConfig.Config(services);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger, DataContext dataContext)
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
if (env.IsDevelopment())
{

View file

@ -1,30 +0,0 @@
@echo off
set installpath=%~dp0
NET SESSION >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo adding firewall rules...
netsh.exe advfirewall firewall add rule name="RealDebridClient" dir=in action=allow program="%installpath%RdtClient.Web.exe" enable=yes > nul
echo installing service...
sc create RealDebridClient binPath="%installpath%RdtClient.Web.exe" start=auto
timeout /t 5 /nobreak > NUL
net start RealDebridClient
) ELSE (
echo ######## ######## ######## ####### ########
echo ## ## ## ## ## ## ## ## ##
echo ## ## ## ## ## ## ## ## ##
echo ###### ######## ######## ## ## ########
echo ## ## ## ## ## ## ## ## ##
echo ## ## ## ## ## ## ## ## ##
echo ######## ## ## ## ## ####### ## ##
echo.
echo.
echo ####### ERROR: ADMINISTRATOR PRIVILEGES REQUIRED #########
echo This script must be run as administrator to work properly!
echo If you're seeing this after clicking on a start menu icon,
echo then right click on the shortcut and select "Run As Administrator".
echo ##########################################################
echo.
PAUSE
EXIT /B 1
)
@echo ON

View file

@ -1,30 +0,0 @@
@echo off
set installpath=%~dp0
NET SESSION >nul 2>&1
IF %ERRORLEVEL% EQU 0 (
echo removing firewall rules...
netsh.exe advfirewall firewall remove rule name="RealDebridClient" > nul
echo removing service...
net stop RealDebridClient
timeout /t 5 /nobreak > NUL
sc delete RealDebridClient
) ELSE (
echo ######## ######## ######## ####### ########
echo ## ## ## ## ## ## ## ## ##
echo ## ## ## ## ## ## ## ## ##
echo ###### ######## ######## ## ## ########
echo ## ## ## ## ## ## ## ## ##
echo ## ## ## ## ## ## ## ## ##
echo ######## ## ## ## ## ####### ## ##
echo.
echo.
echo ####### ERROR: ADMINISTRATOR PRIVILEGES REQUIRED #########
echo This script must be run as administrator to work properly!
echo If you're seeing this after clicking on a start menu icon,
echo then right click on the shortcut and select "Run As Administrator".
echo ##########################################################
echo.
PAUSE
EXIT /B 1
)
@echo ON

View file

@ -1 +0,0 @@
dotnet RdtClient.Web.dll