diff --git a/Shifty.App/Authentication/Authentication.razor b/Shifty.App/Authentication/Authentication.razor new file mode 100644 index 0000000..3f40910 --- /dev/null +++ b/Shifty.App/Authentication/Authentication.razor @@ -0,0 +1,25 @@ +@page "/Auth" +@using Microsoft.AspNetCore.Authorization +@attribute [AllowAnonymous] +@using Shifty.App.Services +@inject NavigationManager NavManager +@inject IAuthenticationService _authenticationService + +@code { + [CascadingParameter] public Task AuthTask { get; set; } + + [SupplyParameterFromQuery(Name = "token")] + public string Token { get; set; } + + [AllowAnonymous] + protected override async Task OnInitializedAsync() + { + if (!string.IsNullOrWhiteSpace(Token)) + { + await _authenticationService.Authenticate(Token); + } + + NavManager.NavigateTo("/"); + } + +} \ 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/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 29ffbde..1352613 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) { - 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 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 7b5ab93..4080ca0 100644 --- a/Shifty.App/Repositories/IAccountRepository.cs +++ b/Shifty.App/Repositories/IAccountRepository.cs @@ -9,8 +9,10 @@ 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); + } } \ No newline at end of file diff --git a/Shifty.App/Services/AuthenticationService.cs b/Shifty.App/Services/AuthenticationService.cs index 7987a3c..a728b30 100644 --- a/Shifty.App/Services/AuthenticationService.cs +++ b/Shifty.App/Services/AuthenticationService.cs @@ -1,11 +1,8 @@ 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 Microsoft.AspNetCore.Authorization; namespace Shifty.App.Services { @@ -21,37 +18,53 @@ 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, 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); - if (either.IsLeft) - { - System.Console.WriteLine(either.Right(w => w.ToString()).Left(e => e.Message)); - return false; - } - - var jwtString = either.ValueUnsafe().Token; - await _localStorage.SetItemAsync("token", jwtString); - return _authStateProvider.UpdateAuthState(jwtString); + return either.Match( + Left: error => + { + return false; + }, + Right: _ => true + ); } public async Task Logout() { await _localStorage.RemoveItemAsync("token"); + await _localStorage.RemoveItemAsync("refreshToken"); _authStateProvider.UpdateAuthState(""); } + + [AllowAnonymous] + public async Task Refresh() + { + var refreshToken = await _localStorage.GetItemAsync("refreshToken"); + return await Authenticate(refreshToken); + } + + [AllowAnonymous] + public async Task Authenticate(string token) + { + var either = await _accountRepository.AuthenticateAsync(token); + + return await either.Match( + Left: e => + { + return Task.FromResult(false); + }, + Right: async response => + { + var jwtString = response.Jwt; + await _localStorage.SetItemAsync("refreshToken", response.RefreshToken); + await _localStorage.SetItemAsync("token", jwtString); + _authStateProvider.UpdateAuthState(jwtString); + + return true; + }); + } } } \ No newline at end of file diff --git a/Shifty.App/Services/IAuthenticationService.cs b/Shifty.App/Services/IAuthenticationService.cs index 00c16e7..cc7e695 100644 --- a/Shifty.App/Services/IAuthenticationService.cs +++ b/Shifty.App/Services/IAuthenticationService.cs @@ -4,7 +4,9 @@ namespace Shifty.App.Services { public interface IAuthenticationService { - Task LoginUser(string username, string password); + Task LoginUser(string username); Task Logout(); + 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 f8ba204..59b0e3c 100644 --- a/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json +++ b/Shifty.Generated.ApiClient/OpenApiSpecs/AnalogCoreV2.json @@ -23,7 +23,9 @@ "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,29 +362,29 @@ "type": "integer", "format": "int32", "default": 30, - "maximum": 100, - "minimum": 1 + "maximum": 100.0, + "minimum": 1.0 }, "x-position": 3 } ], "responses": { - "200": { - "description": "Users, possible with filter applied", + "401": { + "description": " Invalid credentials ", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UserSearchResponse" + "$ref": "#/components/schemas/ApiError" } } } }, - "401": { - "description": " Invalid credentials ", + "200": { + "description": "Users, possible with filter applied", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ApiError" + "$ref": "#/components/schemas/UserSearchResponse" } } } @@ -384,9 +400,88 @@ ] } }, + "/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": { + "post": { + "tags": [ + "Account" + ], + "summary": "Authenticates the user with the token hash from a magic link", + "operationId": "Account_Authenticate", + "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": "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": { + "$ref": "#/components/schemas/UserLoginResponse" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MessageResponseDto" + } + } + } + }, + "default": { + "description": "" + } + } + } + }, "/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 +534,9 @@ }, "/api/v2/appconfig": { "get": { - "tags": ["AppConfig"], + "tags": [ + "AppConfig" + ], "summary": "Get app configuration", "operationId": "AppConfig_Get", "responses": { @@ -458,7 +555,9 @@ }, "/api/v2/health/ping": { "get": { - "tags": ["Health"], + "tags": [ + "Health" + ], "summary": "Ping", "operationId": "Health_Ping", "responses": { @@ -485,7 +584,9 @@ }, "/api/v2/health/check": { "get": { - "tags": ["Health"], + "tags": [ + "Health" + ], "summary": "Check service health", "operationId": "Health_Healthcheck", "responses": { @@ -525,7 +626,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 +677,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 +725,9 @@ }, "/api/v2/menuitems": { "get": { - "tags": ["MenuItems"], + "tags": [ + "MenuItems" + ], "summary": "Returns a list of all menu items", "operationId": "MenuItems_GetAllMenuItems", "responses": { @@ -648,7 +755,9 @@ ] }, "post": { - "tags": ["MenuItems"], + "tags": [ + "MenuItems" + ], "summary": "Adds a menu item", "operationId": "MenuItems_AddMenuItem", "requestBody": { @@ -688,7 +797,9 @@ }, "/api/v2/menuitems/{id}": { "put": { - "tags": ["MenuItems"], + "tags": [ + "MenuItems" + ], "summary": "Updates a menu item", "operationId": "MenuItems_UpdateMenuItem", "parameters": [ @@ -741,7 +852,9 @@ }, "/api/v2/mobilepay/webhook": { "post": { - "tags": ["MobilePay"], + "tags": [ + "MobilePay" + ], "summary": "Webhook to be invoked by MobilePay backend", "operationId": "MobilePay_Webhook", "parameters": [ @@ -792,7 +905,9 @@ }, "/api/v2/products": { "post": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Adds a new product", "operationId": "Products_AddProduct", "requestBody": { @@ -830,7 +945,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 +980,9 @@ }, "/api/v2/products/{id}": { "put": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Updates a product with the specified changes.", "operationId": "Products_UpdateProduct", "parameters": [ @@ -915,7 +1034,9 @@ ] }, "get": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Returns a product with the specified id", "operationId": "Products_GetProduct", "parameters": [ @@ -969,7 +1090,9 @@ }, "/api/v2/products/all": { "get": { - "tags": ["Products"], + "tags": [ + "Products" + ], "summary": "Returns a list of all products", "operationId": "Products_GetAllProducts", "responses": { @@ -999,7 +1122,9 @@ }, "/api/v2/purchases": { "get": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Get all purchases", "operationId": "Purchases_GetAllPurchases", "responses": { @@ -1033,7 +1158,9 @@ ] }, "post": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Initiate a new payment.", "operationId": "Purchases_InitiatePurchase", "requestBody": { @@ -1079,7 +1206,9 @@ }, "/api/v2/purchases/user/{userId}": { "get": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Get all purchases for a user", "operationId": "Purchases_GetAllPurchasesForUser", "parameters": [ @@ -1138,7 +1267,9 @@ }, "/api/v2/purchases/{id}": { "get": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Get purchase", "operationId": "Purchases_GetPurchase", "parameters": [ @@ -1191,7 +1322,9 @@ }, "/api/v2/purchases/{id}/refund": { "put": { - "tags": ["Purchases"], + "tags": [ + "Purchases" + ], "summary": "Refunds a payment", "operationId": "Purchases_RefundPurchase", "parameters": [ @@ -1237,7 +1370,9 @@ }, "/api/v2/tickets": { "get": { - "tags": ["Tickets"], + "tags": [ + "Tickets" + ], "summary": "Returns a list of tickets", "operationId": "Tickets_Get", "parameters": [ @@ -1281,7 +1416,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 +1481,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 +1542,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 +1606,9 @@ }, "/api/v2/webhooks/accounts/user-group": { "put": { - "tags": ["Webhooks"], + "tags": [ + "Webhooks" + ], "summary": "Update user groups in bulk", "operationId": "Webhooks_UpdateUserGroups", "requestBody": { @@ -1485,11 +1628,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 +1674,12 @@ "programmeId": 1 }, "additionalProperties": false, - "required": ["name", "email", "password", "programmeId"], + "required": [ + "name", + "email", + "password", + "programmeId" + ], "properties": { "name": { "type": "string", @@ -1661,8 +1809,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 +1831,11 @@ "fullName": "Software Development" }, "additionalProperties": false, - "required": ["id", "shortName", "fullName"], + "required": [ + "id", + "shortName", + "fullName" + ], "properties": { "id": { "type": "integer", @@ -1748,7 +1910,9 @@ "emailExists": true }, "additionalProperties": false, - "required": ["emailExists"], + "required": [ + "emailExists" + ], "properties": { "emailExists": { "type": "boolean", @@ -1763,7 +1927,9 @@ "email": "johndoe@mail.com" }, "additionalProperties": false, - "required": ["email"], + "required": [ + "email" + ], "properties": { "email": { "type": "string", @@ -1781,7 +1947,9 @@ "UserGroup": "Barista" }, "additionalProperties": false, - "required": ["userGroup"], + "required": [ + "userGroup" + ], "properties": { "userGroup": { "description": "The UserGroup of a user", @@ -1796,9 +1964,19 @@ }, "UserGroup": { "type": "string", - "description": "", - "x-enumNames": ["Customer", "Barista", "Manager", "Board"], - "enum": ["Customer", "Barista", "Manager", "Board"] + "description": "Represents the different groups that a user can belong to.", + "x-enumNames": [ + "Customer", + "Barista", + "Manager", + "Board" + ], + "enum": [ + "Customer", + "Barista", + "Manager", + "Board" + ] }, "ResendAccountVerificationEmailRequest": { "type": "object", @@ -1807,7 +1985,9 @@ "email": "john@doe.com" }, "additionalProperties": false, - "required": ["email"], + "required": [ + "email" + ], "properties": { "email": { "type": "string", @@ -1822,7 +2002,10 @@ "type": "object", "description": "Represents a search result", "additionalProperties": false, - "required": ["totalUsers", "users"], + "required": [ + "totalUsers", + "users" + ], "properties": { "totalUsers": { "type": "integer", @@ -1891,9 +2074,103 @@ }, "UserState": { "type": "string", - "description": "", - "x-enumNames": ["Active", "Deleted", "PendingActivition"], - "enum": ["Active", "Deleted", "PendingActivition"] + "description": "Represents the state of a User.", + "x-enumNames": [ + "Active", + "Deleted", + "PendingActivition" + ], + "enum": [ + "Active", + "Deleted", + "PendingActivition" + ] + }, + "UserLoginRequest": { + "type": "object", + "description": "User login request object", + "example": { + "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": { + "description": "Defines which application should open on login", + "example": "Shifty", + "oneOf": [ + { + "$ref": "#/components/schemas/LoginType" + } + ] + } + } + }, + "LoginType": { + "type": "string", + "description": "Enum for applications to log in to", + "x-enumNames": [ + "Shifty", + "App" + ], + "enum": [ + "Shifty", + "App" + ] + }, + "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", + "minLength": 1 + }, + "refreshToken": { + "type": "string", + "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]" + } + } }, "UnusedClipsResponse": { "type": "object", @@ -1951,7 +2228,9 @@ "environmentType": "Production" }, "additionalProperties": false, - "required": ["environmentType"], + "required": [ + "environmentType" + ], "properties": { "environmentType": { "description": "Environment type for indicating production or test system", @@ -1967,8 +2246,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 +2317,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 +2361,9 @@ "type": "object", "description": "Initiate a new menuitem add request.", "additionalProperties": false, - "required": ["name"], + "required": [ + "name" + ], "properties": { "name": { "type": "string", @@ -2076,7 +2377,10 @@ "type": "object", "description": "Initiate an update product request.", "additionalProperties": false, - "required": ["name", "active"], + "required": [ + "name", + "active" + ], "properties": { "name": { "type": "string", @@ -2162,7 +2466,10 @@ "description": "Coffee clip card of 10 clips", "isPerk": true, "visible": true, - "allowedUserGroups": ["Manager", "Board"], + "allowedUserGroups": [ + "Manager", + "Board" + ], "eligibleMenuItems": [ { "id": 1, @@ -2237,7 +2544,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 +2572,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 +2605,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 +2616,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 +2645,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 +2678,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 +2689,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 +2773,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", @@ -2533,9 +2865,14 @@ "FreePurchasePaymentDetails": "#/components/schemas/FreePurchasePaymentDetails" } }, + "description": "Payment details", "x-abstract": true, "additionalProperties": false, - "required": ["paymentType", "orderId", "discriminator"], + "required": [ + "paymentType", + "orderId", + "discriminator" + ], "properties": { "paymentType": { "description": "Payment type", @@ -2560,8 +2897,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 +2921,10 @@ "paymentId": "186d2b31-ff25-4414-9fd1-bfe9807fa8b7" }, "additionalProperties": false, - "required": ["mobilePayAppRedirectUri", "paymentId"], + "required": [ + "mobilePayAppRedirectUri", + "paymentId" + ], "properties": { "mobilePayAppRedirectUri": { "type": "string", @@ -2694,7 +3040,10 @@ "type": "object", "description": "Initiate a new purchase request", "additionalProperties": false, - "required": ["productId", "paymentType"], + "required": [ + "productId", + "paymentType" + ], "properties": { "productId": { "type": "integer", @@ -2717,7 +3066,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 +3117,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 +3162,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 +3191,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 +3277,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 +3294,10 @@ "type": "object", "description": "Represents an account user group update", "additionalProperties": false, - "required": ["accountId", "userGroup"], + "required": [ + "accountId", + "userGroup" + ], "properties": { "accountId": { "type": "integer", @@ -2962,4 +3334,4 @@ } } } -} +} \ No newline at end of file