mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Rewoking the login workflow
This commit is contained in:
parent
62753b20bb
commit
4bdaba4643
@ -6,6 +6,7 @@
|
||||
<TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
|
||||
<IsPackable>false</IsPackable>
|
||||
<SpaRoot>Views/WebClient/</SpaRoot>
|
||||
<LoginRoot>Views/Login/</LoginRoot>
|
||||
<TranscoderRoot>../transcoder</TranscoderRoot>
|
||||
<DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules/**</DefaultItemExcludes>
|
||||
|
||||
@ -44,6 +45,8 @@
|
||||
<Content Remove="$(SpaRoot)**" />
|
||||
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
|
||||
<StaticFiles Include="$(SpaRoot)static/**" />
|
||||
<LoginFiles Include="$(LoginRoot)**" />
|
||||
<StaticFiles Include="Views\Login\material-icons.css" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
|
||||
@ -64,11 +67,17 @@
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
<ResolvedFileToPublish Include="@(LoginFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
|
||||
<RelativePath>login/%(LoginFiles.RecursiveDir)%(LoginFiles.Filename)%(LoginFiles.Extension)</RelativePath>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
<Target Name="Prepare the web app" AfterTargets="Build" Condition="$(Configuration) == 'Debug'">
|
||||
<Copy SourceFiles="@(StaticFiles)" DestinationFolder="$(OutputPath)/wwwroot/%(RecursiveDir)" />
|
||||
<Copy SourceFiles="@(LoginFiles)" DestinationFolder="$(OutputPath)/login/%(RecursiveDir)" />
|
||||
<Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
|
||||
</Target>
|
||||
|
||||
@ -87,8 +96,4 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CreatePluginFolder" AfterTargets="Build">
|
||||
<MakeDir Directories="$(OutputPath)/plugins" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
@ -22,14 +22,16 @@ namespace Kyoo
|
||||
new Client
|
||||
{
|
||||
ClientId = "kyoo.webapp",
|
||||
|
||||
AccessTokenType = AccessTokenType.Jwt,
|
||||
AllowedGrantTypes = GrantTypes.Code,
|
||||
RequirePkce = true,
|
||||
AllowAccessTokensViaBrowser = true,
|
||||
AlwaysIncludeUserClaimsInIdToken = true,
|
||||
AllowOfflineAccess = true,
|
||||
RequireClientSecret = false,
|
||||
|
||||
AllowAccessTokensViaBrowser = true,
|
||||
AllowOfflineAccess = true,
|
||||
RequireConsent = false,
|
||||
AccessTokenType = AccessTokenType.Jwt,
|
||||
|
||||
AllowedScopes = { "openid", "profile", "kyoo.read", "kyoo.write", "kyoo.play", "kyoo.download", "kyoo.admin" },
|
||||
RedirectUris = { "/", "/silent" },
|
||||
PostLogoutRedirectUris = { "/logout" }
|
||||
|
@ -15,7 +15,7 @@ namespace Kyoo
|
||||
|
||||
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
|
||||
WebHost.CreateDefaultBuilder(args)
|
||||
.UseKestrel((config) => { config.AddServerHeader = false; })
|
||||
.UseKestrel(config => { config.AddServerHeader = false; })
|
||||
.UseUrls("http://*:5000")
|
||||
.UseStartup<Startup>();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.SpaServices.AngularCli;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -145,6 +146,10 @@ namespace Kyoo
|
||||
app.UseAuthentication();
|
||||
app.UseIdentityServer();
|
||||
app.UseAuthorization();
|
||||
app.UseCookiePolicy(new CookiePolicyOptions
|
||||
{
|
||||
MinimumSameSitePolicy = SameSiteMode.Lax
|
||||
});
|
||||
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
@ -11,6 +12,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using SignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
@ -32,7 +34,8 @@ namespace Kyoo.Api
|
||||
|
||||
public class OtacRequest
|
||||
{
|
||||
public string Otac;
|
||||
public string Otac { get; set; }
|
||||
public bool StayLoggedIn { get; set; }
|
||||
}
|
||||
|
||||
public class AccountData
|
||||
@ -45,6 +48,28 @@ namespace Kyoo.Api
|
||||
public IFormFile Picture { get; set; }
|
||||
}
|
||||
|
||||
[ApiController]
|
||||
public class AccountUiController : Controller
|
||||
{
|
||||
[HttpGet("login")]
|
||||
public IActionResult Index()
|
||||
{
|
||||
return new PhysicalFileResult(Path.GetFullPath("login/login.html"), "text/html");
|
||||
}
|
||||
|
||||
[HttpGet("login/{*file}")]
|
||||
public IActionResult Index(string file)
|
||||
{
|
||||
string path = Path.Combine(Path.GetFullPath("login/"), file);
|
||||
if (!System.IO.File.Exists(path))
|
||||
return NotFound();
|
||||
FileExtensionContentTypeProvider provider = new FileExtensionContentTypeProvider();
|
||||
if (!provider.TryGetContentType(path, out string contentType))
|
||||
contentType = "text/plain";
|
||||
return new PhysicalFileResult(path, contentType);
|
||||
}
|
||||
}
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class AccountController : Controller, IProfileService
|
||||
@ -74,6 +99,10 @@ namespace Kyoo.Api
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(user);
|
||||
if (user.Username.Length < 4)
|
||||
return BadRequest(new[] {new {code = "username", description = "Username must be at least 4 characters."}});
|
||||
if (!new EmailAddressAttribute().IsValid(user.Email))
|
||||
return BadRequest(new[] {new {code = "email", description = "Email must be valid."}});
|
||||
User account = new User {UserName = user.Username, Email = user.Email};
|
||||
IdentityResult result = await _userManager.CreateAsync(account, user.Password);
|
||||
if (!result.Succeeded)
|
||||
@ -81,7 +110,7 @@ namespace Kyoo.Api
|
||||
string otac = account.GenerateOTAC(TimeSpan.FromMinutes(1));
|
||||
await _userManager.UpdateAsync(account);
|
||||
await _userManager.AddClaimsAsync(account, defaultClaims);
|
||||
return Ok(otac);
|
||||
return Ok(new {otac});
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
@ -98,12 +127,14 @@ namespace Kyoo.Api
|
||||
[HttpPost("otac-login")]
|
||||
public async Task<IActionResult> OtacLogin([FromBody] OtacRequest otac)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return BadRequest(otac);
|
||||
User user = _userManager.Users.FirstOrDefault(x => x.OTAC == otac.Otac);
|
||||
if (user == null)
|
||||
return BadRequest(new [] { new {code = "InvalidOTAC", description = "No user was found for this OTAC."}});
|
||||
if (user.OTACExpires <= DateTime.UtcNow)
|
||||
return BadRequest(new [] { new {code = "ExpiredOTAC", description = "The OTAC has expired. Try to login with your password."}});
|
||||
await _signInManager.SignInAsync(user, true);
|
||||
await _signInManager.SignInAsync(user, otac.StayLoggedIn);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
1
Kyoo/Views/Login/lib/bootstrap.min.css
vendored
Normal file
1
Kyoo/Views/Login/lib/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
Kyoo/Views/Login/lib/bootstrap.min.js
vendored
Normal file
7
Kyoo/Views/Login/lib/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
Kyoo/Views/Login/lib/jquery.min.js
vendored
Normal file
2
Kyoo/Views/Login/lib/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
16
Kyoo/Views/Login/login.css
Normal file
16
Kyoo/Views/Login/login.css
Normal file
@ -0,0 +1,16 @@
|
||||
.nav-tabs .nav-link, a
|
||||
{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-item.show .nav-link, .nav-tabs .nav-link.active
|
||||
{
|
||||
background-color: #343a40;
|
||||
color: white;
|
||||
border-color: #343a40;
|
||||
}
|
||||
|
||||
.form-control:focus
|
||||
{
|
||||
box-shadow: none;
|
||||
}
|
90
Kyoo/Views/Login/login.html
Normal file
90
Kyoo/Views/Login/login.html
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kyoo - Login</title>
|
||||
<link rel="stylesheet" type="text/css" href="login/lib/bootstrap.min.css" />
|
||||
<link rel="stylesheet" type="text/css" href="login/login.css" />
|
||||
<link rel="stylesheet" type="text/css" href="login/material-icons.css" />
|
||||
<script src="login/lib/jquery.min.js"></script>
|
||||
<script src="login/lib/bootstrap.min.js"></script>
|
||||
</head>
|
||||
<body style="height: 100vh; align-items: center;" class="d-flex">
|
||||
<div class="container pb-5">
|
||||
<div class="card bg-dark mb-5">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<ul class="nav nav-tabs card-header-tabs navbar-dark" role="tablist" id="tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" id="login-tab" data-toggle="tab" href="#login" role="tab" aria-selected="true">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#register" role="tab" aria-selected="false">Register</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-body tab-content">
|
||||
<div class="tab-pane fade show active" id="login" role="tabpanel" aria-labelledby="login-tab">
|
||||
<div class="alert alert-danger" role="alert" id="login-error" style="display: none;"></div>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="login-username">Username</label>
|
||||
<input autocomplete="username" class="form-control" id="login-username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="login-password">Password</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="current-password" type="password" class="form-control" id="login-password">
|
||||
<div class="input-group-append password-visibility">
|
||||
<a class="input-group-text" href="#"><i class="material-icons">visibility</i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember-me">
|
||||
<label class="form-check-label" for="remember-me">Remember me</label>
|
||||
</div>
|
||||
<button id="login-btn" type="submit" class="btn" style="background-color: #e23c00;">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="register" role="tabpanel" aria-labelledby="register-tab">
|
||||
<div class="alert alert-danger" role="alert" id="register-error" style="display: none;"></div>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label for="register-email">Email</label>
|
||||
<input type="email" class="form-control" id="register-email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-username">Username</label>
|
||||
<input autocomplete="username" class="form-control" id="register-username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-password">Password</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="new-password" type="password" class="form-control" id="register-password">
|
||||
<div class="input-group-append password-visibility">
|
||||
<a class="input-group-text" href="#"><i class="material-icons">visibility</i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="register-password-confirm">Confirm Password</label>
|
||||
<div class="input-group">
|
||||
<input autocomplete="new-password" type="password" class="form-control" id="register-password-confirm">
|
||||
<div class="input-group-append password-visibility">
|
||||
<a class="input-group-text" href="#"><i class="material-icons">visibility</i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group form-check">
|
||||
<input type="checkbox" class="form-check-input" id="stay-logged-in">
|
||||
<label class="form-check-label" for="stay-logged-in">Stay logged in</label>
|
||||
</div>
|
||||
<button id="register-btn" type="submit" class="btn" style="background-color: #e23c00;">Register</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="login/login.js"></script>
|
||||
</body>
|
||||
</html>
|
127
Kyoo/Views/Login/login.js
Normal file
127
Kyoo/Views/Login/login.js
Normal file
@ -0,0 +1,127 @@
|
||||
$("#tabs a").on("click", function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
$(this).tab("show");
|
||||
});
|
||||
|
||||
$(".password-visibility a").on("click", function(e)
|
||||
{
|
||||
e.preventDefault();
|
||||
let password = $(this).parent().siblings("input");
|
||||
let toggle = $(this).children("i");
|
||||
|
||||
if (password.attr("type") === "text")
|
||||
{
|
||||
password.attr("type", "password");
|
||||
toggle.text("visibility");
|
||||
}
|
||||
else
|
||||
{
|
||||
toggle.text("visibility_off");
|
||||
password.attr("type", "text");
|
||||
}
|
||||
});
|
||||
|
||||
$("#login-btn").on("click", function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
let user = {
|
||||
username: $("#login-username")[0].value,
|
||||
password: $("#login-password")[0].value,
|
||||
stayLoggedIn: $("#remember-me")[0].checked
|
||||
};
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
url: "/api/account/login",
|
||||
type: "POST",
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
data: JSON.stringify(user),
|
||||
success: function ()
|
||||
{
|
||||
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
|
||||
|
||||
if (returnUrl == null)
|
||||
window.location.href = "/unauthorized";
|
||||
else
|
||||
window.location.href = returnUrl;
|
||||
},
|
||||
error: function(xhr)
|
||||
{
|
||||
let error = $("#login-error");
|
||||
error.show();
|
||||
error.text(JSON.parse(xhr.responseText)[0].description);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#register-btn").on("click", function (e)
|
||||
{
|
||||
e.preventDefault();
|
||||
|
||||
let user = {
|
||||
email: $("#register-email")[0].value,
|
||||
username: $("#register-username")[0].value,
|
||||
password: $("#register-password")[0].value
|
||||
};
|
||||
|
||||
if (user.password !== $("#register-password-confirm")[0].value)
|
||||
{
|
||||
let error = $("#register-error");
|
||||
error.show();
|
||||
error.text("Passwords don't match.");
|
||||
return;
|
||||
}
|
||||
|
||||
$.ajax(
|
||||
{
|
||||
url: "/api/account/register",
|
||||
type: "POST",
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(user),
|
||||
success: function(res)
|
||||
{
|
||||
useOtac(res.otac);
|
||||
},
|
||||
error: function(xhr)
|
||||
{
|
||||
let error = $("#register-error");
|
||||
error.show();
|
||||
error.text(JSON.parse(xhr.responseText)[0].description);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function useOtac(otac)
|
||||
{
|
||||
$.ajax(
|
||||
{
|
||||
url: "/api/account/otac-login",
|
||||
type: "POST",
|
||||
contentType: 'application/json;charset=UTF-8',
|
||||
data: JSON.stringify({otac: otac, tayLoggedIn: $("#stay-logged-in")[0].checked}),
|
||||
success: function()
|
||||
{
|
||||
let returnUrl = new URLSearchParams(window.location.search).get("ReturnUrl");
|
||||
|
||||
if (returnUrl == null)
|
||||
window.location.href = "/unauthorized";
|
||||
else
|
||||
window.location.href = returnUrl;
|
||||
},
|
||||
error: function(xhr)
|
||||
{
|
||||
let error = $("#register-error");
|
||||
error.show();
|
||||
error.text(JSON.parse(xhr.responseText)[0].description);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
let otac = new URLSearchParams(window.location.search).get("otac");
|
||||
if (otac != null)
|
||||
useOtac(otac);
|
36
Kyoo/Views/Login/material-icons.css
Normal file
36
Kyoo/Views/Login/material-icons.css
Normal file
@ -0,0 +1,36 @@
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url(/iconfont/MaterialIcons-Regular.eot); /* For IE6-8 */
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url(/iconfont/MaterialIcons-Regular.woff2) format('woff2'),
|
||||
url(/iconfont/MaterialIcons-Regular.woff) format('woff'),
|
||||
url(/iconfont/MaterialIcons-Regular.ttf) format('truetype');
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
font-family: 'Material Icons';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px; /* Preferred icon size */
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
}
|
@ -1 +1 @@
|
||||
Subproject commit 3f56ef31b8e2cc86d806ef422885ae5c1b802898
|
||||
Subproject commit f5deefafa4d30d1259c44166e23ad5f806ec0768
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"server.urls": "http://0.0.0.0:5000",
|
||||
"public_url": "http://localhost:5000/",
|
||||
"http_port": 5000,
|
||||
"https_port": 44300,
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit a8e443545aa2963d995b6b2937d61a77620899b0
|
||||
Subproject commit 59da0095a85914eabde9900973331a25a16088b3
|
Loading…
x
Reference in New Issue
Block a user