From 5f9d5b03f8102e39f5ae2ab35295664bf614559a Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Fri, 1 Nov 2024 10:56:21 +0100 Subject: [PATCH 1/7] Initial changes for magic-link based authentication --- .../Authentication/Authentication.razor | 35 ++ .../Authentication/CustomAuthStateProvider.cs | 36 +- Shifty.App/Repositories/AccountRepository.cs | 17 +- Shifty.App/Repositories/IAccountRepository.cs | 3 +- Shifty.App/Services/AuthenticationService.cs | 21 +- Shifty.App/Services/IAuthenticationService.cs | 3 + Shifty.App/Shared/MainLayout.razor | 3 + .../OpenApiSpecs/AnalogCoreV2.json | 569 +++++++++++++++--- 8 files changed, 576 insertions(+), 111 deletions(-) create mode 100644 Shifty.App/Authentication/Authentication.razor diff --git a/Shifty.App/Authentication/Authentication.razor b/Shifty.App/Authentication/Authentication.razor new file mode 100644 index 0000000..7f091b1 --- /dev/null +++ b/Shifty.App/Authentication/Authentication.razor @@ -0,0 +1,35 @@ +@page "/Auth" +@using Microsoft.AspNetCore.Authorization +@attribute [AllowAnonymous] +@using Microsoft.AspNetCore.Components +@using Shifty.App.Services +@inject NavigationManager NavManager +@inject IAuthenticationService _authenticationService + +@code { + [CascadingParameter] public Task AuthTask { get; set; } + private System.Security.Claims.ClaimsPrincipal _user; + + [AllowAnonymous] + protected async override Task OnInitializedAsync() + { + var authState = await AuthTask; + _user = authState.User; + + var uri = new Uri(NavManager.Uri); + var query = System.Web.HttpUtility.ParseQueryString(uri.Query); + + var token = query.Get("token"); + + if (string.IsNullOrWhiteSpace(token)) + { + NavManager.NavigateTo("/"); + } + + Console.WriteLine("Calling authentication"); + await _authenticationService.Authenticate(token); + Console.WriteLine("Finished calling authentication"); + + NavManager.NavigateTo("/"); + } +} \ No newline at end of file diff --git a/Shifty.App/Authentication/CustomAuthStateProvider.cs b/Shifty.App/Authentication/CustomAuthStateProvider.cs index 8a48c54..45a1c20 100644 --- a/Shifty.App/Authentication/CustomAuthStateProvider.cs +++ b/Shifty.App/Authentication/CustomAuthStateProvider.cs @@ -3,39 +3,48 @@ using System.Security.Claims; using System.Threading.Tasks; using Blazored.LocalStorage; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; +using Shifty.Api.Generated.AnalogCoreV2; namespace Shifty.App.Authentication { public class CustomAuthStateProvider : AuthenticationStateProvider { private readonly ILocalStorageService _localStorage; + private readonly AnalogCoreV2 _client; - public CustomAuthStateProvider(ILocalStorageService storageService) + public CustomAuthStateProvider(ILocalStorageService storageService, AnalogCoreV2 client) { _localStorage = storageService; + _client = client; } public override async Task GetAuthenticationStateAsync() { var jwtString = await _localStorage.GetItemAsync("token"); - var user = ParseJwtString(jwtString); + var user = await ParseJwtString(jwtString); return new AuthenticationState(user); } - public bool UpdateAuthState(string jwtString) + [AllowAnonymous] + public async Task UpdateAuthState(string jwtString) { - var user = ParseJwtString(jwtString); + var user = await ParseJwtString(jwtString); + Console.WriteLine("User authenticated: " + user.Identity.IsAuthenticated); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); return true; } - private static ClaimsPrincipal ParseJwtString (string jwtString) + private async Task ParseJwtString (string jwtString) { var tokenHandler = new JwtSecurityTokenHandler(); - + Console.WriteLine("Authenticating with token: " + jwtString); if (!tokenHandler.CanReadToken(jwtString)) + { + Console.WriteLine("Cannot read token"); return new ClaimsPrincipal(); + } var token = new JwtSecurityTokenHandler().ReadJwtToken(jwtString); return (DateTime.UtcNow < token.ValidTo) switch @@ -43,6 +52,21 @@ private static ClaimsPrincipal ParseJwtString (string jwtString) true => new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")), //Needs the string passed as well, otherwise the user is not set to authenticated false => new ClaimsPrincipal() }; + // switch(DateTime.UtcNow < token.ValidTo) + // { + // case true: + // return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); //Needs the string passed as well, otherwise the user is not set to authenticated + // case false: + // var t = await _client.ApiV2AccountAuthRefreshAsync(LoginType.Shifty, null); + // if (t is not null) + // { + // await _localStorage.SetItemAsync("token", t.Jwt); + // return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); + // } else + // { + // return new ClaimsPrincipal(); + // } + // } } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index 29ffbde..1adc832 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -20,15 +20,13 @@ public AccountRepository(AnalogCoreV1 v1client, AnalogCoreV2 v2client) _v2client = v2client; } - public async Task> LoginAsync(string username, string password) + public async Task> LoginAsync(string username, string password) { - var dto = new LoginDto() - { - Email= username, - Password = password, - Version = "2.1.0" + var dto = new UserLoginRequest(){ + Email = username }; - return await TryAsync(_v1client.ApiV1AccountLoginAsync(loginDto: dto)).ToEither(); + + return await TryAsync(_v2client.ApiV2AccountLoginAsync(dto)).ToEither(); } public async Task> SearchUserAsync(string query, int page, int pageSize) @@ -40,5 +38,10 @@ public async Task> UpdateUserGroupAsync(int userId, UserGroup group) { return await TryAsync(_v2client.ApiV2AccountUserGroupAsync(userId, new(){UserGroup = group})); } + + public async Task AuthenticateAsync(string token) + { + return await _v2client.ApiV2AccountAuthLoginAsync(token, LoginType.Shifty); + } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index 7b5ab93..be7ea8f 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -9,8 +9,9 @@ namespace Shifty.App.Repositories { public interface IAccountRepository { - public Task> LoginAsync(string username, string password); + public Task> LoginAsync(string username, string password); public Task> SearchUserAsync(string query, int page, int pageSize); public Task> UpdateUserGroupAsync(int userId, UserGroup group); + public Task AuthenticateAsync(string token); } } \ No newline at end of file diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index 7987a3c..6ff13b5 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -6,6 +6,9 @@ using LanguageExt.UnsafeValueAccess; using Shifty.App.Authentication; using Shifty.App.Repositories; +using MudBlazor.Extensions; +using System.Security.Authentication; +using Microsoft.AspNetCore.Authorization; namespace Shifty.App.Services { @@ -43,15 +46,25 @@ public async Task LoginUser(string username, string password) return false; } - var jwtString = either.ValueUnsafe().Token; - await _localStorage.SetItemAsync("token", jwtString); - return _authStateProvider.UpdateAuthState(jwtString); + return true; // Email has been sent, allegedly + + // var jwtString = either.ValueUnsafe().Token; + // await _localStorage.SetItemAsync("token", jwtString); + // return _authStateProvider.UpdateAuthState(jwtString); } public async Task Logout() { await _localStorage.RemoveItemAsync("token"); - _authStateProvider.UpdateAuthState(""); + await _authStateProvider.UpdateAuthState(""); + } + + [AllowAnonymous] + public async Task Authenticate(string token) + { + var res = await _accountRepository.AuthenticateAsync(token); + await _localStorage.SetItemAsync("token", res.Jwt); + await _authStateProvider.UpdateAuthState(res.Jwt); } } } \ No newline at end of file diff --git a/Shifty.App/Services/IAuthenticationService.cs b/Shifty.App/Services/IAuthenticationService.cs index 00c16e7..129d8a7 100644 --- a/Shifty.App/Services/IAuthenticationService.cs +++ b/Shifty.App/Services/IAuthenticationService.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; namespace Shifty.App.Services { @@ -6,5 +7,7 @@ public interface IAuthenticationService { Task LoginUser(string username, string password); Task Logout(); + [AllowAnonymous] + Task Authenticate(string token); } } \ No newline at end of file diff --git a/Shifty.App/Shared/MainLayout.razor b/Shifty.App/Shared/MainLayout.razor index 189eba2..f3a1c68 100644 --- a/Shifty.App/Shared/MainLayout.razor +++ b/Shifty.App/Shared/MainLayout.razor @@ -1,4 +1,7 @@ +@using Shifty.App.Authentication +@using Shifty.App.Pages @using MudBlazor.Utilities +@inject NavigationManager Navigation @inherits LayoutComponentBase diff --git a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json index f8ba204..08161de 100644 --- a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json @@ -17,13 +17,15 @@ }, "servers": [ { - "url": "https://core.dev.analogio.dk" + "url": "http://localhost:5001" } ], "paths": { "/api/v2/account": { "post": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Register data request. An account is required to verify its email before logging in", "operationId": "Account_Register", "requestBody": { @@ -63,7 +65,9 @@ } }, "delete": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Request the deletion of the user coupled to the provided token", "operationId": "Account_Delete", "responses": { @@ -94,7 +98,9 @@ ] }, "get": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Returns basic data about the account", "operationId": "Account_Get", "responses": { @@ -122,7 +128,9 @@ ] }, "put": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Updates the account and returns the updated values.\nOnly properties which are present in the UpdateUserRequest will be updated", "operationId": "Account_Update", "requestBody": { @@ -165,7 +173,9 @@ }, "/api/v2/account/email-exists": { "post": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Check if a given email is in use", "operationId": "Account_EmailExists", "requestBody": { @@ -200,7 +210,9 @@ }, "/api/v2/account/{id}/user-group": { "patch": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Updates the user group of a user", "operationId": "Account_UpdateAccountUserGroup", "parameters": [ @@ -266,7 +278,9 @@ }, "/api/v2/account/resend-verification-email": { "post": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Resend account verification email if account is not already verified", "operationId": "Account_ResendVerificationEmail", "requestBody": { @@ -311,7 +325,9 @@ }, "/api/v2/account/search": { "get": { - "tags": ["Account"], + "tags": [ + "Account" + ], "summary": "Searches a user in the database", "operationId": "Account_SearchUsers", "parameters": [ @@ -322,8 +338,8 @@ "schema": { "type": "integer", "format": "int32", - "maximum": 2147483647, - "minimum": 0 + "maximum": 2147483647.0, + "minimum": 0.0 }, "x-position": 1 }, @@ -346,13 +362,23 @@ "type": "integer", "format": "int32", "default": 30, - "maximum": 100, - "minimum": 1 + "maximum": 100.0, + "minimum": 1.0 }, "x-position": 3 } ], "responses": { + "401": { + "description": " Invalid credentials ", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiError" + } + } + } + }, "200": { "description": "Users, possible with filter applied", "content": { @@ -362,13 +388,132 @@ } } } + } + }, + "security": [ + { + "jwt": [] }, - "401": { - "description": " Invalid credentials ", + { + "apikey": [] + } + ] + } + }, + "/api/v2/account/login": { + "post": { + "tags": [ + "Account" + ], + "summary": "Sends a magic link to the user's email to login", + "operationId": "Account_Login", + "requestBody": { + "x-name": "request", + "description": "User's email", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserLoginRequest" + } + } + }, + "required": true, + "x-position": 1 + }, + "responses": { + "204": { + "description": "" + }, + "default": { + "description": "" + } + } + } + }, + "/api/v2/account/auth/login": { + "get": { + "tags": [ + "Account" + ], + "operationId": "Account_AuthToken", + "parameters": [ + { + "name": "tokenHash", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "loginType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LoginType" + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiError" + "$ref": "#/components/schemas/UserLoginResponse" + } + } + } + }, + "204": { + "description": "" + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponseDto" + } + } + } + } + } + } + }, + "/api/v2/account/auth/refresh": { + "post": { + "tags": [ + "Account" + ], + "operationId": "Account_Refresh", + "parameters": [ + { + "name": "loginType", + "in": "query", + "schema": { + "$ref": "#/components/schemas/LoginType" + }, + "x-position": 1 + }, + { + "name": "refreshToken", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserLoginResponse" } } } @@ -386,7 +531,9 @@ }, "/api/v2/statistics/unused-clips": { "post": { - "tags": ["AdminStatistics"], + "tags": [ + "AdminStatistics" + ], "summary": "Sum unused clip cards within a given period per productId", "operationId": "AdminStatistics_GetUnusedClips", "requestBody": { @@ -439,7 +586,9 @@ }, "/api/v2/appconfig": { "get": { - "tags": ["AppConfig"], + "tags": [ + "AppConfig" + ], "summary": "Get app configuration", "operationId": "AppConfig_Get", "responses": { @@ -458,7 +607,9 @@ }, "/api/v2/health/ping": { "get": { - "tags": ["Health"], + "tags": [ + "Health" + ], "summary": "Ping", "operationId": "Health_Ping", "responses": { @@ -485,7 +636,9 @@ }, "/api/v2/health/check": { "get": { - "tags": ["Health"], + "tags": [ + "Health" + ], "summary": "Check service health", "operationId": "Health_Healthcheck", "responses": { @@ -525,7 +678,9 @@ }, "/api/v2/leaderboard/top": { "get": { - "tags": ["Leaderboard"], + "tags": [ + "Leaderboard" + ], "summary": "Gets the top leaderboard by the specified preset", "operationId": "Leaderboard_GetTopEntries", "parameters": [ @@ -574,7 +729,9 @@ }, "/api/v2/leaderboard": { "get": { - "tags": ["Leaderboard"], + "tags": [ + "Leaderboard" + ], "summary": "Get leaderboard stats for authenticated user. A user will have rank 0 if they do not have any valid swipes", "operationId": "Leaderboard_Get", "parameters": [ @@ -620,7 +777,9 @@ }, "/api/v2/menuitems": { "get": { - "tags": ["MenuItems"], + "tags": [ + "MenuItems" + ], "summary": "Returns a list of all menu items", "operationId": "MenuItems_GetAllMenuItems", "responses": { @@ -648,7 +807,9 @@ ] }, "post": { - "tags": ["MenuItems"], + "tags": [ + "MenuItems" + ], "summary": "Adds a menu item", "operationId": "MenuItems_AddMenuItem", "requestBody": { @@ -688,7 +849,9 @@ }, "/api/v2/menuitems/{id}": { "put": { - "tags": ["MenuItems"], + "tags": [ + "MenuItems" + ], "summary": "Updates a menu item", "operationId": "MenuItems_UpdateMenuItem", "parameters": [ @@ -741,7 +904,9 @@ }, "/api/v2/mobilepay/webhook": { "post": { - "tags": ["MobilePay"], + "tags": [ + "MobilePay" + ], "summary": "Webhook to be invoked by MobilePay backend", "operationId": "MobilePay_Webhook", "parameters": [ @@ -792,7 +957,9 @@ }, "/api/v2/products": { "post": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Adds a new product", "operationId": "Products_AddProduct", "requestBody": { @@ -830,7 +997,9 @@ ] }, "get": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Returns a list of available products based on a account's user group.", "operationId": "Products_GetProducts", "responses": { @@ -863,7 +1032,9 @@ }, "/api/v2/products/{id}": { "put": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Updates a product with the specified changes.", "operationId": "Products_UpdateProduct", "parameters": [ @@ -915,7 +1086,9 @@ ] }, "get": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Returns a product with the specified id", "operationId": "Products_GetProduct", "parameters": [ @@ -969,7 +1142,9 @@ }, "/api/v2/products/all": { "get": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Returns a list of all products", "operationId": "Products_GetAllProducts", "responses": { @@ -999,7 +1174,9 @@ }, "/api/v2/purchases": { "get": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Get all purchases", "operationId": "Purchases_GetAllPurchases", "responses": { @@ -1033,7 +1210,9 @@ ] }, "post": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Initiate a new payment.", "operationId": "Purchases_InitiatePurchase", "requestBody": { @@ -1079,7 +1258,9 @@ }, "/api/v2/purchases/user/{userId}": { "get": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Get all purchases for a user", "operationId": "Purchases_GetAllPurchasesForUser", "parameters": [ @@ -1138,7 +1319,9 @@ }, "/api/v2/purchases/{id}": { "get": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Get purchase", "operationId": "Purchases_GetPurchase", "parameters": [ @@ -1191,7 +1374,9 @@ }, "/api/v2/purchases/{id}/refund": { "put": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Refunds a payment", "operationId": "Purchases_RefundPurchase", "parameters": [ @@ -1237,7 +1422,9 @@ }, "/api/v2/tickets": { "get": { - "tags": ["Tickets"], + "tags": [ + "Tickets" + ], "summary": "Returns a list of tickets", "operationId": "Tickets_Get", "parameters": [ @@ -1281,7 +1468,9 @@ }, "/api/v2/tickets/use": { "post": { - "tags": ["Tickets"], + "tags": [ + "Tickets" + ], "summary": "Uses a ticket (for the given product) on the given menu item", "operationId": "Tickets_UseTicket", "requestBody": { @@ -1344,7 +1533,9 @@ }, "/api/v2/vouchers/issue-vouchers": { "post": { - "tags": ["Vouchers"], + "tags": [ + "Vouchers" + ], "summary": "Issue voucher codes, that can later be redeemed", "operationId": "Vouchers_IssueVouchers", "requestBody": { @@ -1403,7 +1594,9 @@ }, "/api/v2/vouchers/{voucher-code}/redeem": { "post": { - "tags": ["Vouchers"], + "tags": [ + "Vouchers" + ], "summary": "Redeems the voucher supplied as parameter in the path", "operationId": "Vouchers_RedeemVoucher", "parameters": [ @@ -1465,7 +1658,9 @@ }, "/api/v2/webhooks/accounts/user-group": { "put": { - "tags": ["Webhooks"], + "tags": [ + "Webhooks" + ], "summary": "Update user groups in bulk", "operationId": "Webhooks_UpdateUserGroups", "requestBody": { @@ -1485,11 +1680,11 @@ "204": { "description": "The user groups were updated" }, - "400": { - "description": "Bad request. See explanation" - }, "401": { "description": "Invalid credentials" + }, + "400": { + "description": "Bad request. See explanation" } }, "security": [ @@ -1531,7 +1726,12 @@ "programmeId": 1 }, "additionalProperties": false, - "required": ["name", "email", "password", "programmeId"], + "required": [ + "name", + "email", + "password", + "programmeId" + ], "properties": { "name": { "type": "string", @@ -1661,8 +1861,18 @@ "UserRole": { "type": "string", "description": "", - "x-enumNames": ["Customer", "Barista", "Manager", "Board"], - "enum": ["Customer", "Barista", "Manager", "Board"] + "x-enumNames": [ + "Customer", + "Barista", + "Manager", + "Board" + ], + "enum": [ + "Customer", + "Barista", + "Manager", + "Board" + ] }, "ProgrammeResponse": { "type": "object", @@ -1673,7 +1883,11 @@ "fullName": "Software Development" }, "additionalProperties": false, - "required": ["id", "shortName", "fullName"], + "required": [ + "id", + "shortName", + "fullName" + ], "properties": { "id": { "type": "integer", @@ -1748,7 +1962,9 @@ "emailExists": true }, "additionalProperties": false, - "required": ["emailExists"], + "required": [ + "emailExists" + ], "properties": { "emailExists": { "type": "boolean", @@ -1763,7 +1979,9 @@ "email": "johndoe@mail.com" }, "additionalProperties": false, - "required": ["email"], + "required": [ + "email" + ], "properties": { "email": { "type": "string", @@ -1781,7 +1999,9 @@ "UserGroup": "Barista" }, "additionalProperties": false, - "required": ["userGroup"], + "required": [ + "userGroup" + ], "properties": { "userGroup": { "description": "The UserGroup of a user", @@ -1797,8 +2017,18 @@ "UserGroup": { "type": "string", "description": "", - "x-enumNames": ["Customer", "Barista", "Manager", "Board"], - "enum": ["Customer", "Barista", "Manager", "Board"] + "x-enumNames": [ + "Customer", + "Barista", + "Manager", + "Board" + ], + "enum": [ + "Customer", + "Barista", + "Manager", + "Board" + ] }, "ResendAccountVerificationEmailRequest": { "type": "object", @@ -1807,7 +2037,9 @@ "email": "john@doe.com" }, "additionalProperties": false, - "required": ["email"], + "required": [ + "email" + ], "properties": { "email": { "type": "string", @@ -1822,7 +2054,10 @@ "type": "object", "description": "Represents a search result", "additionalProperties": false, - "required": ["totalUsers", "users"], + "required": [ + "totalUsers", + "users" + ], "properties": { "totalUsers": { "type": "integer", @@ -1892,8 +2127,62 @@ "UserState": { "type": "string", "description": "", - "x-enumNames": ["Active", "Deleted", "PendingActivition"], - "enum": ["Active", "Deleted", "PendingActivition"] + "x-enumNames": [ + "Active", + "Deleted", + "PendingActivition" + ], + "enum": [ + "Active", + "Deleted", + "PendingActivition" + ] + }, + "UserLoginRequest": { + "type": "object", + "description": "User login request object", + "example": { + "email": "john@doe.com" + }, + "additionalProperties": false, + "properties": { + "email": { + "type": "string", + "description": "Email of user", + "format": "email", + "example": "john@doe.com" + }, + "loginType": { + "$ref": "#/components/schemas/LoginType" + } + } + }, + "LoginType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Shifty", + "App" + ], + "enum": [ + "Shifty", + "App" + ] + }, + "UserLoginResponse": { + "type": "object", + "additionalProperties": false, + "properties": { + "jwt": { + "type": "string", + "description": "JSON Web Token with claims for the user logging in" + }, + "refreshToken": { + "type": "string", + "description": "User's Display Name", + "example": "Name" + } + } }, "UnusedClipsResponse": { "type": "object", @@ -1951,7 +2240,9 @@ "environmentType": "Production" }, "additionalProperties": false, - "required": ["environmentType"], + "required": [ + "environmentType" + ], "properties": { "environmentType": { "description": "Environment type for indicating production or test system", @@ -1967,8 +2258,16 @@ "EnvironmentType": { "type": "string", "description": "", - "x-enumNames": ["Production", "Test", "LocalDevelopment"], - "enum": ["Production", "Test", "LocalDevelopment"] + "x-enumNames": [ + "Production", + "Test", + "LocalDevelopment" + ], + "enum": [ + "Production", + "Test", + "LocalDevelopment" + ] }, "ServiceHealthResponse": { "type": "object", @@ -2030,14 +2329,26 @@ "LeaderboardPreset": { "type": "string", "description": "Preset for filtering Leaderboard based on date range", - "x-enumNames": ["Month", "Semester", "Total"], - "enum": ["Month", "Semester", "Total"] + "x-enumNames": [ + "Month", + "Semester", + "Total" + ], + "enum": [ + "Month", + "Semester", + "Total" + ] }, "MenuItemResponse": { "type": "object", "description": "Represents a menu item that can be redeemed with a ticket", "additionalProperties": false, - "required": ["id", "name", "active"], + "required": [ + "id", + "name", + "active" + ], "properties": { "id": { "type": "integer", @@ -2062,7 +2373,9 @@ "type": "object", "description": "Initiate a new menuitem add request.", "additionalProperties": false, - "required": ["name"], + "required": [ + "name" + ], "properties": { "name": { "type": "string", @@ -2076,7 +2389,10 @@ "type": "object", "description": "Initiate an update product request.", "additionalProperties": false, - "required": ["name", "active"], + "required": [ + "name", + "active" + ], "properties": { "name": { "type": "string", @@ -2162,7 +2478,10 @@ "description": "Coffee clip card of 10 clips", "isPerk": true, "visible": true, - "allowedUserGroups": ["Manager", "Board"], + "allowedUserGroups": [ + "Manager", + "Board" + ], "eligibleMenuItems": [ { "id": 1, @@ -2237,7 +2556,10 @@ "eligibleMenuItems": { "type": "array", "description": "The menu items that this product can be used on.", - "example": ["Cappuccino", "Caffe Latte"], + "example": [ + "Cappuccino", + "Caffe Latte" + ], "items": { "$ref": "#/components/schemas/MenuItemResponse" } @@ -2262,16 +2584,16 @@ "type": "integer", "description": "Gets or sets the price of the product.", "format": "int32", - "maximum": 2147483647, - "minimum": 0, + "maximum": 2147483647.0, + "minimum": 0.0, "example": 10 }, "numberOfTickets": { "type": "integer", "description": "Gets or sets the number of tickets associated with the product.", "format": "int32", - "maximum": 2147483647, - "minimum": 0, + "maximum": 2147483647.0, + "minimum": 0.0, "example": 5 }, "name": { @@ -2295,7 +2617,10 @@ "allowedUserGroups": { "type": "array", "description": "Gets or sets the user groups that can access the product.", - "example": ["Manager", "Board"], + "example": [ + "Manager", + "Board" + ], "items": { "$ref": "#/components/schemas/UserGroup" } @@ -2303,7 +2628,10 @@ "menuItemIds": { "type": "array", "description": "Gets or sets the menu items that are eligible for the product.", - "example": [1, 2], + "example": [ + 1, + 2 + ], "items": { "type": "integer", "format": "int32" @@ -2329,16 +2657,16 @@ "type": "integer", "description": "Gets or sets the updated price of the product.", "format": "int32", - "maximum": 2147483647, - "minimum": 0, + "maximum": 2147483647.0, + "minimum": 0.0, "example": 10 }, "numberOfTickets": { "type": "integer", "description": "Gets or sets the updated number of tickets associated with the product.", "format": "int32", - "maximum": 2147483647, - "minimum": 0, + "maximum": 2147483647.0, + "minimum": 0.0, "example": 5 }, "name": { @@ -2362,7 +2690,10 @@ "allowedUserGroups": { "type": "array", "description": "Gets or sets the user groups that can access the product.", - "example": ["Manager", "Board"], + "example": [ + "Manager", + "Board" + ], "items": { "$ref": "#/components/schemas/UserGroup" } @@ -2370,7 +2701,10 @@ "menuItemIds": { "type": "array", "description": "Gets or sets the eligible menu items for the product.", - "example": [1, 2], + "example": [ + 1, + 2 + ], "items": { "type": "integer", "format": "int32" @@ -2451,8 +2785,18 @@ "PurchaseStatus": { "type": "string", "description": "Status of purchase", - "x-enumNames": ["Completed", "Cancelled", "PendingPayment", "Refunded"], - "enum": ["Completed", "Cancelled", "PendingPayment", "Refunded"] + "x-enumNames": [ + "Completed", + "Cancelled", + "PendingPayment", + "Refunded" + ], + "enum": [ + "Completed", + "Cancelled", + "PendingPayment", + "Refunded" + ] }, "SinglePurchaseResponse": { "type": "object", @@ -2535,7 +2879,11 @@ }, "x-abstract": true, "additionalProperties": false, - "required": ["paymentType", "orderId", "discriminator"], + "required": [ + "paymentType", + "orderId", + "discriminator" + ], "properties": { "paymentType": { "description": "Payment type", @@ -2560,8 +2908,14 @@ "PaymentType": { "type": "string", "description": "PaymentType represents the type of Payment which is used to fulfill a purchase", - "x-enumNames": ["MobilePay", "FreePurchase"], - "enum": ["MobilePay", "FreePurchase"] + "x-enumNames": [ + "MobilePay", + "FreePurchase" + ], + "enum": [ + "MobilePay", + "FreePurchase" + ] }, "MobilePayPaymentDetails": { "allOf": [ @@ -2578,7 +2932,10 @@ "paymentId": "186d2b31-ff25-4414-9fd1-bfe9807fa8b7" }, "additionalProperties": false, - "required": ["mobilePayAppRedirectUri", "paymentId"], + "required": [ + "mobilePayAppRedirectUri", + "paymentId" + ], "properties": { "mobilePayAppRedirectUri": { "type": "string", @@ -2694,7 +3051,10 @@ "type": "object", "description": "Initiate a new purchase request", "additionalProperties": false, - "required": ["productId", "paymentType"], + "required": [ + "productId", + "paymentType" + ], "properties": { "productId": { "type": "integer", @@ -2717,7 +3077,12 @@ "type": "object", "description": "Representing a ticket for a product", "additionalProperties": false, - "required": ["id", "dateCreated", "productId", "productName"], + "required": [ + "id", + "dateCreated", + "productId", + "productName" + ], "properties": { "id": { "type": "integer", @@ -2763,7 +3128,12 @@ "type": "object", "description": "Representing a used ticket for a product", "additionalProperties": false, - "required": ["id", "dateCreated", "dateUsed", "productName"], + "required": [ + "id", + "dateCreated", + "dateUsed", + "productName" + ], "properties": { "id": { "type": "integer", @@ -2803,7 +3173,10 @@ "type": "object", "description": "Represents a request to use a ticket.", "additionalProperties": false, - "required": ["productId", "menuItemId"], + "required": [ + "productId", + "menuItemId" + ], "properties": { "productId": { "type": "integer", @@ -2829,7 +3202,12 @@ "IssuedAt": "2023-02-07T12:00:00" }, "additionalProperties": false, - "required": ["voucherCode", "productId", "productName", "issuedAt"], + "required": [ + "voucherCode", + "productId", + "productName", + "issuedAt" + ], "properties": { "voucherCode": { "type": "string", @@ -2910,7 +3288,9 @@ "type": "object", "description": "Represents a request to update user groups in bulk", "additionalProperties": false, - "required": ["privilegedUsers"], + "required": [ + "privilegedUsers" + ], "properties": { "privilegedUsers": { "type": "array", @@ -2925,7 +3305,10 @@ "type": "object", "description": "Represents an account user group update", "additionalProperties": false, - "required": ["accountId", "userGroup"], + "required": [ + "accountId", + "userGroup" + ], "properties": { "accountId": { "type": "integer", @@ -2962,4 +3345,4 @@ } } } -} +} \ No newline at end of file From a000628dfe598cace85e52b3ba45f0111c2d7d7d Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Fri, 1 Nov 2024 14:02:43 +0100 Subject: [PATCH 2/7] Use refresh token --- .../Authentication/Authentication.razor | 7 +--- .../Authentication/CustomAuthStateProvider.cs | 41 ++++++++----------- Shifty.App/Repositories/AccountRepository.cs | 5 +++ Shifty.App/Repositories/IAccountRepository.cs | 2 + Shifty.App/Services/AuthenticationService.cs | 13 ++++++ Shifty.App/Services/IAuthenticationService.cs | 1 + 6 files changed, 40 insertions(+), 29 deletions(-) diff --git a/Shifty.App/Authentication/Authentication.razor b/Shifty.App/Authentication/Authentication.razor index 7f091b1..c295671 100644 --- a/Shifty.App/Authentication/Authentication.razor +++ b/Shifty.App/Authentication/Authentication.razor @@ -21,14 +21,11 @@ var token = query.Get("token"); - if (string.IsNullOrWhiteSpace(token)) + if (!string.IsNullOrWhiteSpace(token)) { - NavManager.NavigateTo("/"); + await _authenticationService.Authenticate(token); } - Console.WriteLine("Calling authentication"); - await _authenticationService.Authenticate(token); - Console.WriteLine("Finished calling authentication"); NavManager.NavigateTo("/"); } diff --git a/Shifty.App/Authentication/CustomAuthStateProvider.cs b/Shifty.App/Authentication/CustomAuthStateProvider.cs index 45a1c20..5279d85 100644 --- a/Shifty.App/Authentication/CustomAuthStateProvider.cs +++ b/Shifty.App/Authentication/CustomAuthStateProvider.cs @@ -6,18 +6,19 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; using Shifty.Api.Generated.AnalogCoreV2; +using Shifty.App.Services; namespace Shifty.App.Authentication { public class CustomAuthStateProvider : AuthenticationStateProvider { private readonly ILocalStorageService _localStorage; - private readonly AnalogCoreV2 _client; + private readonly IAuthenticationService _authService; - public CustomAuthStateProvider(ILocalStorageService storageService, AnalogCoreV2 client) + public CustomAuthStateProvider(ILocalStorageService storageService, IAuthenticationService authService) { _localStorage = storageService; - _client = client; + _authService = authService; } public override async Task GetAuthenticationStateAsync() @@ -39,34 +40,26 @@ public async Task UpdateAuthState(string jwtString) private async Task ParseJwtString (string jwtString) { var tokenHandler = new JwtSecurityTokenHandler(); - Console.WriteLine("Authenticating with token: " + jwtString); if (!tokenHandler.CanReadToken(jwtString)) { - Console.WriteLine("Cannot read token"); return new ClaimsPrincipal(); } var token = new JwtSecurityTokenHandler().ReadJwtToken(jwtString); - return (DateTime.UtcNow < token.ValidTo) switch + switch(DateTime.UtcNow < token.ValidTo) { - true => new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")), //Needs the string passed as well, otherwise the user is not set to authenticated - false => new ClaimsPrincipal() - }; - // switch(DateTime.UtcNow < token.ValidTo) - // { - // case true: - // return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); //Needs the string passed as well, otherwise the user is not set to authenticated - // case false: - // var t = await _client.ApiV2AccountAuthRefreshAsync(LoginType.Shifty, null); - // if (t is not null) - // { - // await _localStorage.SetItemAsync("token", t.Jwt); - // return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); - // } else - // { - // return new ClaimsPrincipal(); - // } - // } + case true: + return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); //Needs the string passed as well, otherwise the user is not set to authenticated + case false: + var t = await _authService.Refresh(); + if (t) + { + return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); + } else + { + return new ClaimsPrincipal(); + } + } } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index 1adc832..6a2dfd5 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -43,5 +43,10 @@ public async Task AuthenticateAsync(string token) { return await _v2client.ApiV2AccountAuthLoginAsync(token, LoginType.Shifty); } + + public async Task> RefreshTokenAsync() + { + return await TryAsync(_v2client.ApiV2AccountAuthRefreshAsync(LoginType.Shifty, null)); + } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index be7ea8f..f4376ab 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -13,5 +13,7 @@ public interface IAccountRepository public Task> SearchUserAsync(string query, int page, int pageSize); public Task> UpdateUserGroupAsync(int userId, UserGroup group); public Task AuthenticateAsync(string token); + public Task> RefreshTokenAsync(); + } } \ No newline at end of file diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index 6ff13b5..fe2f264 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -66,5 +66,18 @@ public async Task Authenticate(string token) await _localStorage.SetItemAsync("token", res.Jwt); await _authStateProvider.UpdateAuthState(res.Jwt); } + + public async Task Refresh() + { + var res = await _accountRepository.RefreshTokenAsync(); + return await res.Match( + Succ: async (res) => { + await _localStorage.SetItemAsync("token", res.Jwt); + await _authStateProvider.UpdateAuthState(res.Jwt); + return true; + }, + Fail: (err) => Task.FromResult(false) + ); + } } } \ No newline at end of file diff --git a/Shifty.App/Services/IAuthenticationService.cs b/Shifty.App/Services/IAuthenticationService.cs index 129d8a7..2ce5dfa 100644 --- a/Shifty.App/Services/IAuthenticationService.cs +++ b/Shifty.App/Services/IAuthenticationService.cs @@ -9,5 +9,6 @@ public interface IAuthenticationService Task Logout(); [AllowAnonymous] Task Authenticate(string token); + Task Refresh(); } } \ No newline at end of file From 3ed6ad70348388632a4aa5ac140ae88c0526e861 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 5 Nov 2024 18:46:36 +0100 Subject: [PATCH 3/7] refreshToken cookie is still not set --- .../Authentication/CustomAuthStateProvider.cs | 36 +++++-------------- Shifty.App/Components/RedirectToLogin.razor | 13 +++++-- Shifty.App/Repositories/AccountRepository.cs | 4 +-- Shifty.App/Repositories/IAccountRepository.cs | 2 +- Shifty.App/Services/AuthenticationService.cs | 27 ++++++++------ 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/Shifty.App/Authentication/CustomAuthStateProvider.cs b/Shifty.App/Authentication/CustomAuthStateProvider.cs index 5279d85..b164489 100644 --- a/Shifty.App/Authentication/CustomAuthStateProvider.cs +++ b/Shifty.App/Authentication/CustomAuthStateProvider.cs @@ -3,63 +3,45 @@ using System.Security.Claims; using System.Threading.Tasks; using Blazored.LocalStorage; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components.Authorization; -using Shifty.Api.Generated.AnalogCoreV2; -using Shifty.App.Services; namespace Shifty.App.Authentication { public class CustomAuthStateProvider : AuthenticationStateProvider { private readonly ILocalStorageService _localStorage; - private readonly IAuthenticationService _authService; - public CustomAuthStateProvider(ILocalStorageService storageService, IAuthenticationService authService) + public CustomAuthStateProvider(ILocalStorageService storageService) { _localStorage = storageService; - _authService = authService; } public override async Task GetAuthenticationStateAsync() { var jwtString = await _localStorage.GetItemAsync("token"); - var user = await ParseJwtString(jwtString); + var user = ParseJwtString(jwtString); return new AuthenticationState(user); } - [AllowAnonymous] - public async Task UpdateAuthState(string jwtString) + public bool UpdateAuthState(string jwtString) { - var user = await ParseJwtString(jwtString); - Console.WriteLine("User authenticated: " + user.Identity.IsAuthenticated); + var user = ParseJwtString(jwtString); NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user))); return true; } - private async Task ParseJwtString (string jwtString) + private static ClaimsPrincipal ParseJwtString (string jwtString) { var tokenHandler = new JwtSecurityTokenHandler(); if (!tokenHandler.CanReadToken(jwtString)) - { return new ClaimsPrincipal(); - } var token = new JwtSecurityTokenHandler().ReadJwtToken(jwtString); - switch(DateTime.UtcNow < token.ValidTo) + return (DateTime.UtcNow < token.ValidTo) switch { - case true: - return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); //Needs the string passed as well, otherwise the user is not set to authenticated - case false: - var t = await _authService.Refresh(); - if (t) - { - return new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")); - } else - { - return new ClaimsPrincipal(); - } - } + true => new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")), //Needs the string passed as well, otherwise the user is not set to authenticated + false => new ClaimsPrincipal() + }; } } } \ No newline at end of file diff --git a/Shifty.App/Components/RedirectToLogin.razor b/Shifty.App/Components/RedirectToLogin.razor index fd85df9..a512da2 100644 --- a/Shifty.App/Components/RedirectToLogin.razor +++ b/Shifty.App/Components/RedirectToLogin.razor @@ -1,8 +1,17 @@ +@using Shifty.App.Services @inject NavigationManager Navigation +@inject IAuthenticationService _authenticationService @code { - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { - Navigation.NavigateTo("/login", false); + if (await _authenticationService.Refresh()) + { + Navigation.NavigateTo("/", false); + } + else + { + Navigation.NavigateTo("/login", false); + } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index 6a2dfd5..1651f3d 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -44,9 +44,9 @@ public async Task AuthenticateAsync(string token) return await _v2client.ApiV2AccountAuthLoginAsync(token, LoginType.Shifty); } - public async Task> RefreshTokenAsync() + public async Task> RefreshTokenAsync() { - return await TryAsync(_v2client.ApiV2AccountAuthRefreshAsync(LoginType.Shifty, null)); + return await TryAsync(_v2client.ApiV2AccountAuthRefreshAsync(LoginType.Shifty, null)).ToEither(); } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index f4376ab..aa8d7f0 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -13,7 +13,7 @@ public interface IAccountRepository public Task> SearchUserAsync(string query, int page, int pageSize); public Task> UpdateUserGroupAsync(int userId, UserGroup group); public Task AuthenticateAsync(string token); - public Task> RefreshTokenAsync(); + public Task> RefreshTokenAsync(); } } \ No newline at end of file diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index fe2f264..f618234 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -56,7 +56,7 @@ public async Task LoginUser(string username, string password) public async Task Logout() { await _localStorage.RemoveItemAsync("token"); - await _authStateProvider.UpdateAuthState(""); + _authStateProvider.UpdateAuthState(""); } [AllowAnonymous] @@ -64,20 +64,27 @@ public async Task Authenticate(string token) { var res = await _accountRepository.AuthenticateAsync(token); await _localStorage.SetItemAsync("token", res.Jwt); - await _authStateProvider.UpdateAuthState(res.Jwt); + _authStateProvider.UpdateAuthState(res.Jwt); } public async Task Refresh() { + Console.WriteLine("Refreshing token"); var res = await _accountRepository.RefreshTokenAsync(); - return await res.Match( - Succ: async (res) => { - await _localStorage.SetItemAsync("token", res.Jwt); - await _authStateProvider.UpdateAuthState(res.Jwt); - return true; - }, - Fail: (err) => Task.FromResult(false) - ); + + if (res.IsLeft) + { + System.Console.WriteLine(res.Right(w => w.ToString()).Left(e => e.Message)); + return false; + } + + Console.WriteLine("Refreshing token successful"); + + var jwtString = res.ValueUnsafe().Jwt; + await _localStorage.SetItemAsync("token", jwtString); + _authStateProvider.UpdateAuthState(jwtString); + + return true; } } } \ No newline at end of file From 1f407c4bc32edb4603f62574dd1dc51a9e4735fd Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Tue, 5 Nov 2024 19:34:48 +0100 Subject: [PATCH 4/7] Use localstorage for refresh tokens --- Shifty.App/Repositories/AccountRepository.cs | 4 +-- Shifty.App/Repositories/IAccountRepository.cs | 2 +- Shifty.App/Services/AuthenticationService.cs | 8 +++-- Shifty.App/wwwroot/appsettings.json | 2 +- .../OpenApiSpecs/AnalogCoreV2.json | 33 +++++++++---------- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index 1651f3d..f6a62b4 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -44,9 +44,9 @@ public async Task AuthenticateAsync(string token) return await _v2client.ApiV2AccountAuthLoginAsync(token, LoginType.Shifty); } - public async Task> RefreshTokenAsync() + public async Task> RefreshTokenAsync(string refreshToken) { - return await TryAsync(_v2client.ApiV2AccountAuthRefreshAsync(LoginType.Shifty, null)).ToEither(); + return await TryAsync(_v2client.ApiV2AccountAuthRefreshAsync(refreshToken)).ToEither(); } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index aa8d7f0..353b62b 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -13,7 +13,7 @@ public interface IAccountRepository public Task> SearchUserAsync(string query, int page, int pageSize); public Task> UpdateUserGroupAsync(int userId, UserGroup group); public Task AuthenticateAsync(string token); - public Task> RefreshTokenAsync(); + public Task> RefreshTokenAsync(string refreshToken); } } \ No newline at end of file diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index f618234..3087f01 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -64,13 +64,15 @@ public async Task Authenticate(string token) { var res = await _accountRepository.AuthenticateAsync(token); await _localStorage.SetItemAsync("token", res.Jwt); + await _localStorage.SetItemAsync("refreshToken", res.RefreshToken); _authStateProvider.UpdateAuthState(res.Jwt); } public async Task Refresh() { Console.WriteLine("Refreshing token"); - var res = await _accountRepository.RefreshTokenAsync(); + var refreshToken = await _localStorage.GetItemAsync("refreshToken"); + var res = await _accountRepository.RefreshTokenAsync(refreshToken); if (res.IsLeft) { @@ -80,7 +82,9 @@ public async Task Refresh() Console.WriteLine("Refreshing token successful"); - var jwtString = res.ValueUnsafe().Jwt; + var actualRes = res.ValueUnsafe(); + var jwtString = actualRes.Jwt; + await _localStorage.SetItemAsync("refreshToken", actualRes.RefreshToken); await _localStorage.SetItemAsync("token", jwtString); _authStateProvider.UpdateAuthState(jwtString); diff --git a/Shifty.App/wwwroot/appsettings.json b/Shifty.App/wwwroot/appsettings.json index b219808..15f1960 100644 --- a/Shifty.App/wwwroot/appsettings.json +++ b/Shifty.App/wwwroot/appsettings.json @@ -1,3 +1,3 @@ { - "ApiHost": "https://core.dev.analogio.dk/" + "ApiHost": "https://core.local.analogio.dk:5001" } diff --git a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json index 08161de..40ec156 100644 --- a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json @@ -17,7 +17,7 @@ }, "servers": [ { - "url": "http://localhost:5001" + "url": "https://localhost:5001" } ], "paths": { @@ -489,14 +489,6 @@ ], "operationId": "Account_Refresh", "parameters": [ - { - "name": "loginType", - "in": "query", - "schema": { - "$ref": "#/components/schemas/LoginType" - }, - "x-position": 1 - }, { "name": "refreshToken", "in": "query", @@ -504,7 +496,7 @@ "type": "string", "nullable": true }, - "x-position": 2 + "x-position": 1 } ], "responses": { @@ -517,16 +509,21 @@ } } } - } - }, - "security": [ - { - "jwt": [] }, - { - "apikey": [] + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponseDto" + } + } + } + }, + "default": { + "description": "" } - ] + } } }, "/api/v2/statistics/unused-clips": { From 0e3fa8a182c568aae1d744b1fa8adcc696bb9848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Tr=C3=B8strup?= Date: Tue, 5 Nov 2024 21:42:09 +0100 Subject: [PATCH 5/7] Some refactor and use new auth endpoint --- .../Authentication/Authentication.razor | 20 +++---- Shifty.App/Repositories/AccountRepository.cs | 9 +-- Shifty.App/Repositories/IAccountRepository.cs | 3 +- Shifty.App/Services/AuthenticationService.cs | 58 +++++++++--------- Shifty.App/Services/IAuthenticationService.cs | 2 +- .../OpenApiSpecs/AnalogCoreV2.json | 60 ++----------------- 6 files changed, 46 insertions(+), 106 deletions(-) diff --git a/Shifty.App/Authentication/Authentication.razor b/Shifty.App/Authentication/Authentication.razor index c295671..81d138d 100644 --- a/Shifty.App/Authentication/Authentication.razor +++ b/Shifty.App/Authentication/Authentication.razor @@ -8,25 +8,19 @@ @code { [CascadingParameter] public Task AuthTask { get; set; } - private System.Security.Claims.ClaimsPrincipal _user; + + [SupplyParameterFromQuery(Name = "token")] + public string Token { get; set; } [AllowAnonymous] - protected async override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { - var authState = await AuthTask; - _user = authState.User; - - var uri = new Uri(NavManager.Uri); - var query = System.Web.HttpUtility.ParseQueryString(uri.Query); - - var token = query.Get("token"); - - if (!string.IsNullOrWhiteSpace(token)) + if (!string.IsNullOrWhiteSpace(Token)) { - await _authenticationService.Authenticate(token); + await _authenticationService.Authenticate(Token); } - NavManager.NavigateTo("/"); } + } \ No newline at end of file diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index f6a62b4..fadc861 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -39,14 +39,9 @@ public async Task> UpdateUserGroupAsync(int userId, UserGroup group) return await TryAsync(_v2client.ApiV2AccountUserGroupAsync(userId, new(){UserGroup = group})); } - public async Task AuthenticateAsync(string token) + public async Task> AuthenticateAsync(string token) { - return await _v2client.ApiV2AccountAuthLoginAsync(token, LoginType.Shifty); - } - - public async Task> RefreshTokenAsync(string refreshToken) - { - return await TryAsync(_v2client.ApiV2AccountAuthRefreshAsync(refreshToken)).ToEither(); + return await TryAsync(_v2client.ApiV2AccountAuthAsync(token)).ToEither(); } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index 353b62b..83bdda0 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -12,8 +12,7 @@ public interface IAccountRepository public Task> LoginAsync(string username, string password); public Task> SearchUserAsync(string query, int page, int pageSize); public Task> UpdateUserGroupAsync(int userId, UserGroup group); - public Task AuthenticateAsync(string token); - public Task> RefreshTokenAsync(string refreshToken); + public Task> AuthenticateAsync(string token); } } \ No newline at end of file diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index 3087f01..5a4e294 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -40,13 +40,14 @@ public async Task LoginUser(string username, string password) var encodedPassword = EncodePasscode(password); var either = await _accountRepository.LoginAsync(username, encodedPassword); - if (either.IsLeft) - { - System.Console.WriteLine(either.Right(w => w.ToString()).Left(e => e.Message)); - return false; - } - - return true; // Email has been sent, allegedly + return either.Match( + Left: error => + { + Console.WriteLine(error); + return false; + }, + Right: _ => true + ); // var jwtString = either.ValueUnsafe().Token; // await _localStorage.SetItemAsync("token", jwtString); @@ -56,39 +57,42 @@ public async Task LoginUser(string username, string password) public async Task Logout() { await _localStorage.RemoveItemAsync("token"); + await _localStorage.RemoveItemAsync("refreshToken"); _authStateProvider.UpdateAuthState(""); } [AllowAnonymous] - public async Task Authenticate(string token) + public async Task Refresh() { - var res = await _accountRepository.AuthenticateAsync(token); - await _localStorage.SetItemAsync("token", res.Jwt); - await _localStorage.SetItemAsync("refreshToken", res.RefreshToken); - _authStateProvider.UpdateAuthState(res.Jwt); + var refreshToken = await _localStorage.GetItemAsync("refreshToken"); + return await Authenticate(refreshToken); } - public async Task Refresh() + [AllowAnonymous] + public async Task Authenticate(string token) { Console.WriteLine("Refreshing token"); - var refreshToken = await _localStorage.GetItemAsync("refreshToken"); - var res = await _accountRepository.RefreshTokenAsync(refreshToken); + Console.WriteLine(token); - if (res.IsLeft) - { - System.Console.WriteLine(res.Right(w => w.ToString()).Left(e => e.Message)); - return false; - } + var either = await _accountRepository.AuthenticateAsync(token); - Console.WriteLine("Refreshing token successful"); + return await either.Match( + Left: e => + { + Console.WriteLine(e.Message); + return Task.FromResult(false); + }, + Right: async response => + { + Console.WriteLine("Refreshing token successful"); - var actualRes = res.ValueUnsafe(); - var jwtString = actualRes.Jwt; - await _localStorage.SetItemAsync("refreshToken", actualRes.RefreshToken); - await _localStorage.SetItemAsync("token", jwtString); - _authStateProvider.UpdateAuthState(jwtString); + var jwtString = response.Jwt; + await _localStorage.SetItemAsync("refreshToken", response.RefreshToken); + await _localStorage.SetItemAsync("token", jwtString); + _authStateProvider.UpdateAuthState(jwtString); - return true; + return true; + }); } } } \ No newline at end of file diff --git a/Shifty.App/Services/IAuthenticationService.cs b/Shifty.App/Services/IAuthenticationService.cs index 2ce5dfa..66cc77a 100644 --- a/Shifty.App/Services/IAuthenticationService.cs +++ b/Shifty.App/Services/IAuthenticationService.cs @@ -8,7 +8,7 @@ public interface IAuthenticationService Task LoginUser(string username, string password); Task Logout(); [AllowAnonymous] - Task Authenticate(string token); + Task Authenticate(string token); Task Refresh(); } } \ No newline at end of file diff --git a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json index 40ec156..463c7fd 100644 --- a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json @@ -17,7 +17,7 @@ }, "servers": [ { - "url": "https://localhost:5001" + "url": "https://localhost:8081" } ], "paths": { @@ -430,67 +430,15 @@ } } }, - "/api/v2/account/auth/login": { - "get": { - "tags": [ - "Account" - ], - "operationId": "Account_AuthToken", - "parameters": [ - { - "name": "tokenHash", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - }, - { - "name": "loginType", - "in": "query", - "schema": { - "$ref": "#/components/schemas/LoginType" - }, - "x-position": 2 - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UserLoginResponse" - } - } - } - }, - "204": { - "description": "" - }, - "404": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MessageResponseDto" - } - } - } - } - } - } - }, - "/api/v2/account/auth/refresh": { + "/api/v2/account/auth": { "post": { "tags": [ "Account" ], - "operationId": "Account_Refresh", + "operationId": "Account_Authenticate", "parameters": [ { - "name": "refreshToken", + "name": "tokenHash", "in": "query", "schema": { "type": "string", From 23727e66be6cdb6b3b93146d92a555093f4a4b90 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Mon, 28 Apr 2025 20:10:09 +0200 Subject: [PATCH 6/7] Remove password --- Shifty.App/Pages/Login.razor | 10 +-- Shifty.App/Repositories/AccountRepository.cs | 4 +- Shifty.App/Repositories/IAccountRepository.cs | 2 +- Shifty.App/Services/AuthenticationService.cs | 5 +- Shifty.App/Services/IAuthenticationService.cs | 2 +- .../OpenApiSpecs/AnalogCoreV2.json | 84 ++++++++++++++----- 6 files changed, 71 insertions(+), 36 deletions(-) diff --git a/Shifty.App/Pages/Login.razor b/Shifty.App/Pages/Login.razor index c2256e7..44b823d 100644 --- a/Shifty.App/Pages/Login.razor +++ b/Shifty.App/Pages/Login.razor @@ -30,11 +30,6 @@ For="() => _loginForm.Email" Immediate="true" DebounceInterval="500"/> - } @@ -60,16 +55,13 @@ [Required] [EmailAddress] public string Email { get; set; } - - [Required] - public string Password { get; set; } } async Task LoginUser() { _successfulLogin = true; _loggingIn = true; - _successfulLogin = await _authenticationService.LoginUser(_loginForm.Email, _loginForm.Password); + _successfulLogin = await _authenticationService.LoginUser(_loginForm.Email); _loggingIn = false; if (_successfulLogin) { diff --git a/Shifty.App/Repositories/AccountRepository.cs b/Shifty.App/Repositories/AccountRepository.cs index fadc861..1352613 100755 --- a/Shifty.App/Repositories/AccountRepository.cs +++ b/Shifty.App/Repositories/AccountRepository.cs @@ -20,7 +20,7 @@ public AccountRepository(AnalogCoreV1 v1client, AnalogCoreV2 v2client) _v2client = v2client; } - public async Task> LoginAsync(string username, string password) + public async Task> LoginAsync(string username) { var dto = new UserLoginRequest(){ Email = username @@ -41,7 +41,7 @@ public async Task> UpdateUserGroupAsync(int userId, UserGroup group) public async Task> AuthenticateAsync(string token) { - return await TryAsync(_v2client.ApiV2AccountAuthAsync(token)).ToEither(); + return await TryAsync(_v2client.ApiV2AccountAuthAsync(new(){Token = token})).ToEither(); } } } \ No newline at end of file diff --git a/Shifty.App/Repositories/IAccountRepository.cs b/Shifty.App/Repositories/IAccountRepository.cs index 83bdda0..4080ca0 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -9,7 +9,7 @@ namespace Shifty.App.Repositories { public interface IAccountRepository { - public Task> LoginAsync(string username, string password); + public Task> LoginAsync(string username); public Task> SearchUserAsync(string query, int page, int pageSize); public Task> UpdateUserGroupAsync(int userId, UserGroup group); public Task> AuthenticateAsync(string token); diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index 5a4e294..67ef64a 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -35,10 +35,9 @@ private static string EncodePasscode(string passcode) } } - public async Task LoginUser(string username, string password) + public async Task LoginUser(string username) { - var encodedPassword = EncodePasscode(password); - var either = await _accountRepository.LoginAsync(username, encodedPassword); + var either = await _accountRepository.LoginAsync(username); return either.Match( Left: error => diff --git a/Shifty.App/Services/IAuthenticationService.cs b/Shifty.App/Services/IAuthenticationService.cs index 66cc77a..961df0e 100644 --- a/Shifty.App/Services/IAuthenticationService.cs +++ b/Shifty.App/Services/IAuthenticationService.cs @@ -5,7 +5,7 @@ namespace Shifty.App.Services { public interface IAuthenticationService { - Task LoginUser(string username, string password); + Task LoginUser(string username); Task Logout(); [AllowAnonymous] Task Authenticate(string token); diff --git a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json index 463c7fd..59b0e3c 100644 --- a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json @@ -17,7 +17,7 @@ }, "servers": [ { - "url": "https://localhost:8081" + "url": "https://core.dev.analogio.dk" } ], "paths": { @@ -435,21 +435,24 @@ "tags": [ "Account" ], + "summary": "Authenticates the user with the token hash from a magic link", "operationId": "Account_Authenticate", - "parameters": [ - { - "name": "tokenHash", - "in": "query", - "schema": { - "type": "string", - "nullable": true - }, - "x-position": 1 - } - ], + "requestBody": { + "x-name": "token", + "description": "The token hash from the magic link", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TokenLoginRequest" + } + } + }, + "required": true, + "x-position": 1 + }, "responses": { "200": { - "description": "", + "description": "A JSON Web Token used to authenticate for other endpoints and a refresh token to re-authenticate without a new magic link", "content": { "application/json": { "schema": { @@ -1961,7 +1964,7 @@ }, "UserGroup": { "type": "string", - "description": "", + "description": "Represents the different groups that a user can belong to.", "x-enumNames": [ "Customer", "Barista", @@ -2071,7 +2074,7 @@ }, "UserState": { "type": "string", - "description": "", + "description": "Represents the state of a User.", "x-enumNames": [ "Active", "Deleted", @@ -2090,21 +2093,32 @@ "email": "john@doe.com" }, "additionalProperties": false, + "required": [ + "email", + "loginType" + ], "properties": { "email": { "type": "string", "description": "Email of user", "format": "email", + "minLength": 1, "example": "john@doe.com" }, "loginType": { - "$ref": "#/components/schemas/LoginType" + "description": "Defines which application should open on login", + "example": "Shifty", + "oneOf": [ + { + "$ref": "#/components/schemas/LoginType" + } + ] } } }, "LoginType": { "type": "string", - "description": "", + "description": "Enum for applications to log in to", "x-enumNames": [ "Shifty", "App" @@ -2116,16 +2130,45 @@ }, "UserLoginResponse": { "type": "object", + "description": "User login response object", + "example": { + "jwt": "[no example provided]", + "refreshToken": "[no example provided]" + }, "additionalProperties": false, + "required": [ + "jwt", + "refreshToken" + ], "properties": { "jwt": { "type": "string", - "description": "JSON Web Token with claims for the user logging in" + "description": "JSON Web Token with claims for the user logging in", + "minLength": 1 }, "refreshToken": { "type": "string", - "description": "User's Display Name", - "example": "Name" + "description": "Token used to obtain a new JWT token on expiration", + "minLength": 1 + } + } + }, + "TokenLoginRequest": { + "type": "object", + "description": "Magic link request object", + "example": { + "token": "[no example provided]" + }, + "additionalProperties": false, + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "Magic link token", + "minLength": 1, + "example": "[no example provided]" } } }, @@ -2822,6 +2865,7 @@ "FreePurchasePaymentDetails": "#/components/schemas/FreePurchasePaymentDetails" } }, + "description": "Payment details", "x-abstract": true, "additionalProperties": false, "required": [ From 9704fe995ac1d301626e350a272b964cd619b537 Mon Sep 17 00:00:00 2001 From: A-Guldborg Date: Mon, 28 Apr 2025 20:23:29 +0200 Subject: [PATCH 7/7] Cleanup --- .../Authentication/Authentication.razor | 1 - .../Authentication/CustomAuthStateProvider.cs | 3 ++- Shifty.App/Services/AuthenticationService.cs | 27 ------------------- Shifty.App/Services/IAuthenticationService.cs | 2 -- Shifty.App/Shared/MainLayout.razor | 3 --- Shifty.App/wwwroot/appsettings.json | 2 +- 6 files changed, 3 insertions(+), 35 deletions(-) diff --git a/Shifty.App/Authentication/Authentication.razor b/Shifty.App/Authentication/Authentication.razor index 81d138d..3f40910 100644 --- a/Shifty.App/Authentication/Authentication.razor +++ b/Shifty.App/Authentication/Authentication.razor @@ -1,7 +1,6 @@ @page "/Auth" @using Microsoft.AspNetCore.Authorization @attribute [AllowAnonymous] -@using Microsoft.AspNetCore.Components @using Shifty.App.Services @inject NavigationManager NavManager @inject IAuthenticationService _authenticationService diff --git a/Shifty.App/Authentication/CustomAuthStateProvider.cs b/Shifty.App/Authentication/CustomAuthStateProvider.cs index b164489..8a48c54 100644 --- a/Shifty.App/Authentication/CustomAuthStateProvider.cs +++ b/Shifty.App/Authentication/CustomAuthStateProvider.cs @@ -33,13 +33,14 @@ public bool UpdateAuthState(string jwtString) private static ClaimsPrincipal ParseJwtString (string jwtString) { var tokenHandler = new JwtSecurityTokenHandler(); + if (!tokenHandler.CanReadToken(jwtString)) return new ClaimsPrincipal(); var token = new JwtSecurityTokenHandler().ReadJwtToken(jwtString); return (DateTime.UtcNow < token.ValidTo) switch { - true => new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")), //Needs the string passed as well, otherwise the user is not set to authenticated + true => new ClaimsPrincipal(new ClaimsIdentity(token.Claims, "bearerToken")), //Needs the string passed as well, otherwise the user is not set to authenticated false => new ClaimsPrincipal() }; } diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index 67ef64a..a728b30 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -1,13 +1,7 @@ using System.Threading.Tasks; -using System; -using System.Security.Cryptography; -using System.Text; using Blazored.LocalStorage; -using LanguageExt.UnsafeValueAccess; using Shifty.App.Authentication; using Shifty.App.Repositories; -using MudBlazor.Extensions; -using System.Security.Authentication; using Microsoft.AspNetCore.Authorization; namespace Shifty.App.Services @@ -24,16 +18,6 @@ public AuthenticationService(IAccountRepository accountRepository, CustomAuthSta _authStateProvider = stateProvider; _localStorage = storageService; } - - private static string EncodePasscode(string passcode) - { - byte[] bytes = Encoding.UTF8.GetBytes(passcode); - using (SHA256 sha256 = SHA256.Create()) - { - byte[] passcodeHash = sha256.ComputeHash(bytes); - return Convert.ToBase64String(passcodeHash); - } - } public async Task LoginUser(string username) { @@ -42,15 +26,10 @@ public async Task LoginUser(string username) return either.Match( Left: error => { - Console.WriteLine(error); return false; }, Right: _ => true ); - - // var jwtString = either.ValueUnsafe().Token; - // await _localStorage.SetItemAsync("token", jwtString); - // return _authStateProvider.UpdateAuthState(jwtString); } public async Task Logout() @@ -70,21 +49,15 @@ public async Task Refresh() [AllowAnonymous] public async Task Authenticate(string token) { - Console.WriteLine("Refreshing token"); - Console.WriteLine(token); - var either = await _accountRepository.AuthenticateAsync(token); return await either.Match( Left: e => { - Console.WriteLine(e.Message); return Task.FromResult(false); }, Right: async response => { - Console.WriteLine("Refreshing token successful"); - var jwtString = response.Jwt; await _localStorage.SetItemAsync("refreshToken", response.RefreshToken); await _localStorage.SetItemAsync("token", jwtString); diff --git a/Shifty.App/Services/IAuthenticationService.cs b/Shifty.App/Services/IAuthenticationService.cs index 961df0e..cc7e695 100644 --- a/Shifty.App/Services/IAuthenticationService.cs +++ b/Shifty.App/Services/IAuthenticationService.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; namespace Shifty.App.Services { @@ -7,7 +6,6 @@ public interface IAuthenticationService { Task LoginUser(string username); Task Logout(); - [AllowAnonymous] Task Authenticate(string token); Task Refresh(); } diff --git a/Shifty.App/Shared/MainLayout.razor b/Shifty.App/Shared/MainLayout.razor index f3a1c68..189eba2 100644 --- a/Shifty.App/Shared/MainLayout.razor +++ b/Shifty.App/Shared/MainLayout.razor @@ -1,7 +1,4 @@ -@using Shifty.App.Authentication -@using Shifty.App.Pages @using MudBlazor.Utilities -@inject NavigationManager Navigation @inherits LayoutComponentBase diff --git a/Shifty.App/wwwroot/appsettings.json b/Shifty.App/wwwroot/appsettings.json index 15f1960..b219808 100644 --- a/Shifty.App/wwwroot/appsettings.json +++ b/Shifty.App/wwwroot/appsettings.json @@ -1,3 +1,3 @@ { - "ApiHost": "https://core.local.analogio.dk:5001" + "ApiHost": "https://core.dev.analogio.dk/" }