aboutsummaryrefslogtreecommitdiffstats
path: root/src/server
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2020-08-09 15:51:33 +0200
committerivarlovlie <git@ivarlovlie.no>2020-08-09 15:51:33 +0200
commit8614d18522441543e08c37c68121fed1fa8d6ae7 (patch)
treedd53ae13bdf269098e385107d27dcc2a0d8d73db /src/server
parent9b2c6f550a3a705e02dc4f86797c9223ad59d5fa (diff)
downloaddough-8614d18522441543e08c37c68121fed1fa8d6ae7.tar.xz
dough-8614d18522441543e08c37c68121fed1fa8d6ae7.zip
auth user
Diffstat (limited to 'src/server')
-rw-r--r--src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml16
-rw-r--r--src/server/Controllers/AccountController.cs71
-rw-r--r--src/server/Controllers/BaseController.cs2
-rw-r--r--src/server/Dough.csproj11
-rw-r--r--src/server/IdentityServer/Config.cs8
-rw-r--r--src/server/Models/Constants.cs8
-rw-r--r--src/server/Models/Payloads/LoginPayload.cs2
-rw-r--r--src/server/Models/Results/ErrorResult.cs8
-rw-r--r--src/server/Pages/Error.cshtml19
-rw-r--r--src/server/Pages/Error.cshtml.cs12
-rw-r--r--src/server/Pages/Login.cshtml95
-rw-r--r--src/server/Pages/Login.cshtml.cs12
-rw-r--r--src/server/Services/EmailService.cs35
-rw-r--r--src/server/Startup.cs45
-rw-r--r--src/server/tempkey.jwk1
15 files changed, 305 insertions, 40 deletions
diff --git a/src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml b/src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml
new file mode 100644
index 0000000..51115c2
--- /dev/null
+++ b/src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<key id="75aa5a6c-406f-4e39-b1df-58a8d3e6af7a" version="1">
+ <creationDate>2020-08-09T12:29:14.3954895Z</creationDate>
+ <activationDate>2020-08-09T12:29:14.3177582Z</activationDate>
+ <expirationDate>2020-11-07T12:29:14.3177582Z</expirationDate>
+ <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60">
+ <descriptor>
+ <encryption algorithm="AES_256_CBC" />
+ <validation algorithm="HMACSHA256" />
+ <masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection">
+ <!-- Warning: the key below is in an unencrypted form. -->
+ <value>iO4MQasV7DgDGtCJg0PE5yYk/skUTg3Hvzk1gxTzq2sVxDSysKKKJGYuT0k5yctxWlGnOACrGeKyBhTelz3hbA==</value>
+ </masterKey>
+ </descriptor>
+ </descriptor>
+</key> \ No newline at end of file
diff --git a/src/server/Controllers/AccountController.cs b/src/server/Controllers/AccountController.cs
index fe7b7a2..5c760e2 100644
--- a/src/server/Controllers/AccountController.cs
+++ b/src/server/Controllers/AccountController.cs
@@ -1,11 +1,20 @@
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Dough.Models;
using Dough.Models.Database;
+using Dough.Models.Payloads;
+using Dough.Models.Results;
+using Dough.Services;
using Dough.Utilities;
+using IdentityServer4;
using IdentityServer4.Services;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Http;
namespace Dough.Controllers
{
@@ -13,34 +22,68 @@ namespace Dough.Controllers
public class AccountController : BaseController
{
private readonly MainDbContext _context;
- private readonly IIdentityServerInteractionService _identityServerInteractionService;
+ private readonly IIdentityServerInteractionService _interaction;
+ private readonly EmailService _emailService;
public AccountController(MainDbContext context,
- IIdentityServerInteractionService identityServerInteractionService)
+ IIdentityServerInteractionService interaction,
+ EmailService emailService)
{
_context = context;
- _identityServerInteractionService = identityServerInteractionService;
+ _interaction = interaction;
+ _emailService = emailService;
}
+ [HttpGet("login")]
+ public ActionResult GetLogin()
+ {
+ var pathToLoginFile = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "login.html");
+ var fileContent = System.IO.File.ReadAllText(pathToLoginFile);
+ return Content(fileContent, "text/html");
+ }
- // This is the default route for identityserver4 logins (https://identityserver4.readthedocs.io/en/latest/topics/signin.html#login-workflow)
[HttpPost("login")]
- public async Task<ActionResult> Login(string returnUrl)
+ [ValidateAntiForgeryToken]
+ public async Task<ActionResult> PostLogin(LoginPayload payload)
{
- if (returnUrl.IsMissing() || !_identityServerInteractionService.IsValidReturnUrl(returnUrl))
- return BadRequest("route parameter returnUrl is invalid");
+ if (!_interaction.IsValidReturnUrl(payload.ReturnUrl))
+ return BadRequest(new ErrorResult());
+ var user = _context.Users.SingleByNameOrDefault(payload.Username);
+ if (user == default)
+ {
+ await Task.Delay(1500);
+ return BadRequest(new ErrorResult("Username or password is incorrect","Please try again with a different username and/or password"));
+ }
- Console.WriteLine("returnUrl: " + returnUrl);
- var reqBody = await HttpContext.Request.ReadFormAsync();
- foreach (var formEl in reqBody)
+ if (!user.VerifyPassword(payload.Password))
{
- Console.WriteLine(formEl.Key);
- foreach (var value in formEl.Value)
- Console.WriteLine(" - " + value);
+ await Task.Delay(1000);
+ return BadRequest(new ErrorResult("Username or password is incorrect","Please try again with a different username and/or password"));
}
- return Ok();
+
+ var props = new AuthenticationProperties
+ {
+ AllowRefresh = true,
+ IssuedUtc = DateTime.UtcNow,
+ };
+
+ if (payload.Persist)
+ {
+ props.IsPersistent = true;
+ props.ExpiresUtc = DateTime.UtcNow.AddDays(15);
+ }
+
+ var identityServerUser = new IdentityServerUser(user.Id.ToString())
+ {
+ DisplayName = user.Username,
+ AuthenticationTime = DateTime.UtcNow,
+ };
+
+ await HttpContext.SignInAsync(identityServerUser, props);
+
+ return Ok(payload.ReturnUrl);
}
diff --git a/src/server/Controllers/BaseController.cs b/src/server/Controllers/BaseController.cs
index 046c060..44b11b2 100644
--- a/src/server/Controllers/BaseController.cs
+++ b/src/server/Controllers/BaseController.cs
@@ -6,7 +6,7 @@ using Dough.Utilities;
namespace Dough.Controllers
{
[ApiController]
- [Route("api/[controller]")]
+ [Route("[controller]")]
public class BaseController : ControllerBase
{
public LoggedInUserModel LoggedInUser => new LoggedInUserModel
diff --git a/src/server/Dough.csproj b/src/server/Dough.csproj
index e54ff49..407dc84 100644
--- a/src/server/Dough.csproj
+++ b/src/server/Dough.csproj
@@ -8,6 +8,7 @@
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0" />
<PackageReference Include="IdentityServer4" Version="4.0.4" />
<PackageReference Include="IdentityServer4.EntityFramework" Version="4.0.4" />
+ <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.6" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.6" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" />
<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
@@ -15,6 +16,16 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6" />
</ItemGroup>
<ItemGroup>
+ <Content Update="Pages\Login.cshtml">
+ <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+ </Content>
+ <Content Update="Pages\Error.cshtml">
+ <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
+ <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
+ </Content>
+ </ItemGroup>
+ <ItemGroup>
<Folder Include="AppData" />
</ItemGroup>
</Project> \ No newline at end of file
diff --git a/src/server/IdentityServer/Config.cs b/src/server/IdentityServer/Config.cs
index 41363f1..5b2bf13 100644
--- a/src/server/IdentityServer/Config.cs
+++ b/src/server/IdentityServer/Config.cs
@@ -22,11 +22,17 @@ namespace Dough.IdentityServer
RedirectUris = Constants.BrowserAppLoginRedirectUrls,
PostLogoutRedirectUris = Constants.BrowserAppLogoutRedirectUrls,
AllowedCorsOrigins = Constants.BrowserAppUrls,
+ AccessTokenType = AccessTokenType.Reference,
+ RequireConsent = false,
+ RefreshTokenExpiration = TokenExpiration.Sliding,
+ AlwaysSendClientClaims = true,
+ AllowOfflineAccess = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
+ IdentityServerConstants.StandardScopes.OfflineAccess,
MainApiScopeName
}
}
@@ -45,4 +51,4 @@ namespace Dough.IdentityServer
new IdentityResources.Profile(),
};
}
-}
+} \ No newline at end of file
diff --git a/src/server/Models/Constants.cs b/src/server/Models/Constants.cs
index 3afaaad..04e8a2b 100644
--- a/src/server/Models/Constants.cs
+++ b/src/server/Models/Constants.cs
@@ -11,13 +11,13 @@ namespace Dough.Models
};
public static readonly string[] BrowserAppLoginRedirectUrls =
{
- "http://localhost:8080/signin-oidc",
- "http://localhost:3000/signin-oidc",
+ "http://localhost:8080/oidc-callback",
+ "http://localhost:3000/oidc-callback",
};
public static readonly string[] BrowserAppLogoutRedirectUrls =
{
- "http://localhost:8080/signout-callback-oidc",
- "http://localhost:3000/signout-callback-oidc",
+ "http://localhost:8080",
+ "http://localhost:3000",
};
}
diff --git a/src/server/Models/Payloads/LoginPayload.cs b/src/server/Models/Payloads/LoginPayload.cs
index d7bc50b..1a112a6 100644
--- a/src/server/Models/Payloads/LoginPayload.cs
+++ b/src/server/Models/Payloads/LoginPayload.cs
@@ -4,5 +4,7 @@ namespace Dough.Models.Payloads
{
public string Username { get; set; }
public string Password { get; set; }
+ public string ReturnUrl { get; set; }
+ public bool Persist { get; set; }
}
} \ No newline at end of file
diff --git a/src/server/Models/Results/ErrorResult.cs b/src/server/Models/Results/ErrorResult.cs
index 8d4504f..edd447c 100644
--- a/src/server/Models/Results/ErrorResult.cs
+++ b/src/server/Models/Results/ErrorResult.cs
@@ -2,13 +2,13 @@ namespace Dough.Models.Results
{
public class ErrorResult
{
- public ErrorResult(string title = default, string message = default)
+ public ErrorResult(string title = "Something went wrong", string message = "Please try again soon")
{
Title = title;
Message = message;
}
- public string Title { get; set; } = "En feil oppstod";
- public string Message { get; set; } = "Vennligst prøv igjen snart";
+ public string Title { get; set; }
+ public string Message { get; set; }
}
-}
+} \ No newline at end of file
diff --git a/src/server/Pages/Error.cshtml b/src/server/Pages/Error.cshtml
new file mode 100644
index 0000000..6030139
--- /dev/null
+++ b/src/server/Pages/Error.cshtml
@@ -0,0 +1,19 @@
+@page
+@model Dough.Pages.Error
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+<head>
+ <title></title>
+</head>
+<body>
+<div>
+
+</div>
+</body>
+</html> \ No newline at end of file
diff --git a/src/server/Pages/Error.cshtml.cs b/src/server/Pages/Error.cshtml.cs
new file mode 100644
index 0000000..5ec2c40
--- /dev/null
+++ b/src/server/Pages/Error.cshtml.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Dough.Pages
+{
+ public class Error : PageModel
+ {
+ public void OnGet()
+ {
+
+ }
+ }
+} \ No newline at end of file
diff --git a/src/server/Pages/Login.cshtml b/src/server/Pages/Login.cshtml
new file mode 100644
index 0000000..d3829a2
--- /dev/null
+++ b/src/server/Pages/Login.cshtml
@@ -0,0 +1,95 @@
+@page
+@model Dough.Pages.Login
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Login - dough</title>
+ <style>
+ form {
+ display: flex;
+ flex-direction: column;
+ max-width: 350px;
+ margin: 0 auto;
+ }
+
+ form label {
+ padding-top: 15px;
+ }
+
+ button {
+ margin-top:15px;
+ }
+ </style>
+</head>
+<body>
+<div id="error" style="display: none">
+ <h2 id="title"></h2>
+ <p id="message"></p>
+</div>
+<form onsubmit="return false">
+ <label for="username">Username: </label>
+ <input type="text" id="username" name="username" required autofocus>
+ <label for="password">Password: </label>
+ <input type="password" id="password" name="password" required>
+ <label for="persist">Remeber me: <input type="checkbox" id="persist" name="persist"/></label>
+ @Html.AntiForgeryToken()
+ <button>Login</button>
+</form>
+
+<script>
+ const form = document.querySelector("form");
+ const errorEL = document.querySelector("#error");
+ const titleEl = document.querySelector("#title");
+ const messageEl =document.querySelector("#message");
+ form.addEventListener("submit", () => {
+ const username = document.querySelector("#username").value;
+ const password = document.querySelector("#password").value;
+ const returnUrl = new URL(location.href).searchParams.get("ReturnUrl");
+ const persist = document.querySelector("#persist").checked;
+ let data = {
+ username,
+ password,
+ returnUrl,
+ persist
+ };
+ errorEL.style.diplay = "none";
+
+ fetch("/account/login", {
+ method: "POST",
+ body: JSON.stringify(data),
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json;charset=utf-8",
+ "RequestVerificationToken": document.querySelector("input[name='__RequestVerificationToken']").value
+ }
+ }).then(response => {
+ if(response.status === 400) {
+ response.json().then(res => {
+ if(res.title && res.message) {
+ displayError(res.title, res.message);
+ }
+ })
+ } else {
+ location.href = returnUrl;
+ }
+ }).catch(error => console.error(error));
+ })
+
+ function displayError(title, message) {
+ const errorEL = document.querySelector("#error");
+ const titleEl = document.querySelector("#title");
+ const messageEl =document.querySelector("#message");
+ titleEl.innerText = title;
+ messageEl.innerText = message;
+ errorEL.style.display = "inline-block";
+ }
+</script>
+
+</body>
+</html> \ No newline at end of file
diff --git a/src/server/Pages/Login.cshtml.cs b/src/server/Pages/Login.cshtml.cs
new file mode 100644
index 0000000..02d5ee1
--- /dev/null
+++ b/src/server/Pages/Login.cshtml.cs
@@ -0,0 +1,12 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace Dough.Pages
+{
+ public class Login : PageModel
+ {
+ public void OnGet()
+ {
+
+ }
+ }
+} \ No newline at end of file
diff --git a/src/server/Services/EmailService.cs b/src/server/Services/EmailService.cs
index 0d70f0f..9d795d6 100644
--- a/src/server/Services/EmailService.cs
+++ b/src/server/Services/EmailService.cs
@@ -1,7 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Configuration;
+
namespace Dough.Services
{
public class EmailService
{
-
+ private readonly IConfiguration _configuration;
+
+ public EmailService(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public async Task<bool> Send(string subject, string email)
+ {
+ var password = _configuration.GetValue<string>("");
+ var emailUser = _configuration.GetValue<string>("");
+ var emailHost = _configuration.GetValue<string>("");
+
+ var httpClient = new HttpClient();
+
+ var payload = new FormUrlEncodedContent(new[]
+ {
+ new KeyValuePair<string, string>("username", emailUser),
+ new KeyValuePair<string, string>("password", password),
+ });
+
+ var requestUri = new Uri(emailHost);
+ var request = await httpClient.PostAsync(requestUri, payload);
+
+ return request.IsSuccessStatusCode;
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/server/Startup.cs b/src/server/Startup.cs
index f55a761..891965b 100644
--- a/src/server/Startup.cs
+++ b/src/server/Startup.cs
@@ -1,3 +1,4 @@
+using System.IO;
using Dough.IdentityServer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -7,6 +8,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Dough.Models;
using Dough.Models.Database;
+using Dough.Services;
+using IdentityServer4.Configuration;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,7 +24,7 @@ namespace Dough
}
public IConfiguration Configuration { get; }
-
+
private const string DefaultCorsPolicy = "DefaultCorsPolicy";
private string GetConnectionStringFromEnvironment()
@@ -35,7 +39,6 @@ namespace Dough
public void ConfigureServices(IServiceCollection services)
{
-
services.AddCors(options =>
{
options.AddPolicy(DefaultCorsPolicy, builder =>
@@ -48,25 +51,34 @@ namespace Dough
});
});
+ var dataprotectionkeyPath = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "dpkeys");
+ services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(dataprotectionkeyPath));
+
services.AddHealthChecks()
.AddDbContextCheck<MainDbContext>();
- services.AddDbContext<MainDbContext>(options => {
- options.UseMySql(GetConnectionStringFromEnvironment());
- });
-
- services.Configure<ApiBehaviorOptions>(options =>
+ services.AddDbContext<MainDbContext>(options =>
{
- options.SuppressModelStateInvalidFilter = true;
- options.SuppressInferBindingSourcesForParameters = true;
+ options.UseMySql(GetConnectionStringFromEnvironment());
});
- var builder = services.AddIdentityServer()
+
+ services.AddIdentityServer(options =>
+ {
+ options.UserInteraction = new UserInteractionOptions
+ {
+ LoginUrl = "/login",
+ ErrorUrl = "/error",
+ };
+ })
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
+ .AddDeveloperSigningCredential()
.AddInMemoryClients(Config.Clients);
-
+ services.AddSingleton<EmailService>();
+
services.AddControllers();
+ services.AddRazorPages().AddRazorRuntimeCompilation();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
@@ -78,9 +90,14 @@ namespace Dough
app.UseCors(DefaultCorsPolicy);
app.UseHealthChecks("/health");
app.UseStatusCodePages();
- app.UseAuthentication();
+ app.UseIdentityServer();
app.UseAuthorization();
- app.UseEndpoints(endpoints => { endpoints.MapControllers().RequireAuthorization(); });
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapRazorPages();
+ endpoints.MapControllers()
+ .RequireAuthorization();
+ });
}
}
-}
+} \ No newline at end of file
diff --git a/src/server/tempkey.jwk b/src/server/tempkey.jwk
new file mode 100644
index 0000000..43ef569
--- /dev/null
+++ b/src/server/tempkey.jwk
@@ -0,0 +1 @@
+{"alg":"RS256","d":"wWiBhJo4kDImxwFXqpjJNTRgkb8-f0daqMsVghuMyh9U93JI0bcEHvym4tG9yEAEd4dQXXvXG9SMkx-lQ41GKtkd6U6ZDjFa0QQuH-rGt4Q0MCwYAhFIpzeMZwoVtz0811cWg2QattvZW7zaz5SkjX-CaQ5VJFSLHtMlsPzkE5xxqRPddGK83qXkyEAa0EzmOnCqlwVRtXrP2AgvqhkUjJM-vg0wjp_Bd204C_ECvi09EAB04jor2w3a4zXMIqsJFi2LQwy-1tG6HY8msgKeL-zfkE-ilCCLtWbDnsMEmFdFWksnBnqY-T855O7EjoaI7NXnUW6MvX_EEg_Mn21ioQ","dp":"etVHep7VLlt0AMz0dihBayeYUEa3FzpiIB8AqkB94lMWeM5u1xlrRrMt5UzKmXnT80Bn3HGMPxZ22gIbpmBZceQ2rG2fhQwR86en0a5LW2eL9IltmfOILsMYSDGTcUSPYfXjJjxzjhtbVufNtm4UsjgfEUrozAp4vntkWpxBYzE","dq":"gbQ-JWMX1OMpJY8AkzVvpMAysyqO3pb33UqTv6D9Q7vhPzagwpAZDhXu9B2PW3cRUWkjyiSKy7VzlUvNo-t88-lFbKk40k0xs6OSQvS5cWr_gChdytvhZw8HFzwej8oJ2ZKQGI2_nExgxmtlD7JSSI56VnG2JK37GHQXunsCxWE","e":"AQAB","kid":"E8C61E059FD40310181EDCDF860FEFFC","kty":"RSA","n":"zds1h4_ELIOkVouKRe7FJR4IlxHajw_3BSvFv6lRWsLJvP42nT1dkAffvB4JeJHNObtEITY1kKy5mAcWDo_o82ZFkzTAzKnme3F5U9RKJdimuKYBX2fX2zbjFQobKbaG-YWsrOaFzclvw0qn-APgHJ8E-7AAE90bbeDNs7GgGDkYb8pWsdz6xVcGJ4z1gYo-68bCyg4ki60s69b69d-IYlvElvKXX0wRKFokdsfzntHVlb-oXWH58peffOZEDLHlwVgojvUcItXXKTb0_tXTh4XD-Eh3xL7S_s1vPjxK0c8wk_55nQ5ib5EP16rZtCV4ZWpLPk2DSL4prnHDdE2J_w","p":"68iIC3HGTbcL1XpdB5vDJBp6sqhIU4KFooWulIA_mt96rtzFc4wv7cDH0BeiHGBBmff0lEr8c178RP1brOQe6LF_yZDoZqkt9hWmivDJYbElLRqasjF5M5EFYNgnAuydY5G58E-c-_zkQk6GUpj0YVTX1fcq_Hhs4Sdi4ol8Lek","q":"34HGqU-SF-AboSwXD_rZgy0ReEf03GQYee5zvLhcAa1SJCHRNoBY8mxTNgqT7mFyHC4FuunY6DNChcH8Ooq4zpuwBQ1_zu4Kj2HwGsGeQDfm36K4FDRslIT-24MAMBdvfhdT-OCbXohTs2Z_sp1A6GtJl2XcLehOBGkB7dE9f6c","qi":"GxlYgDzfeRT_b99IXLOv4BbVmRe1CmuuJkoY5iP0EENscaaE5Qz5iAh_DDs72TPE7Hwdp957QRkWWr8yDC_T0-ZVpVIBGNdUtl3rB18USmSFYn4aY57cFzY5Qs9O7jKrGp8jbZNiD5BVSExhEKxNUdBbsadEWWJrLKl-YB4yShw"} \ No newline at end of file