├── app ├── core ├── Views │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Home │ │ ├── Error.cshtml │ │ ├── Index.cshtml │ │ ├── Resp.cshtml │ │ ├── Guide.cshtml │ │ └── Req.cshtml │ └── Shared │ │ └── _Layout.cshtml ├── global.json ├── wwwroot │ ├── sage-developers.png │ └── sample_app.css ├── appsettings.json ├── client_application.template.json ├── appsettings.Development.json ├── app.csproj ├── Models │ └── ContentModel.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Controllers │ └── HomeController.cs └── Startup.cs ├── script ├── restart.sh ├── setup.sh ├── stop.sh ├── build.sh └── start.sh ├── .travis.yml ├── docker-compose.yml ├── Dockerfile ├── .gitignore ├── .vscode ├── tasks.json └── launch.json ├── app.sln ├── LICENSE └── README.md /app/core: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /script/restart.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | ./script/stop.sh 3 | ./script/start.sh -------------------------------------------------------------------------------- /app/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "3.1.200" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using app 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | script: 4 | - ./script/setup.sh 5 | - ./script/build.sh 6 | -------------------------------------------------------------------------------- /app/wwwroot/sage-developers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sage/sageone_api_csharp_sample/HEAD/app/wwwroot/sage-developers.png -------------------------------------------------------------------------------- /app/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /script/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo Building docker image ... 4 | docker build --rm -t sage_accounting_csharp_sample -f Dockerfile . 5 | 6 | echo Setup complete. 7 | -------------------------------------------------------------------------------- /app/client_application.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "client_id": "YOUR_CLIENT_ID", 4 | "client_secret": "YOUR_CLIENT_SECRET", 5 | "callback_url": "http://localhost:8080/auth/callback" 6 | } 7 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | app: 5 | image: ${DOCKER_REGISTRY-}app 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | ports: 10 | - "8080:80" -------------------------------------------------------------------------------- /app/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /script/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo Stopping container ... 4 | docker stop sage_accounting_csharp_sample 5 | docker rm sage_accounting_csharp_sample 6 | 7 | # remove build folder 8 | rm -r app/bin 9 | rm -r app/obj 10 | -------------------------------------------------------------------------------- /app/Views/Home/Error.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | @model app.Models.ContentModel 3 | @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor 4 | @{ 5 | ViewData["Title"] = "Guide"; 6 | } 7 |

Error!!

-------------------------------------------------------------------------------- /app/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model app.Models.ContentModel 2 | @{ 3 | ViewData["Title"] = "Home Page"; 4 | } 5 | 6 | @Html.Partial("Guide") 7 |
8 | @Html.Partial("Req") 9 | @Html.Partial("Resp") 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/core/sdk:3.1.200 AS build 2 | WORKDIR /application 3 | 4 | # copy csproj and restore as distinct layers 5 | COPY *.sln . 6 | COPY app/*.csproj ./app/ 7 | WORKDIR /application/app 8 | # RUN dotnet clean 9 | RUN dotnet restore 10 | -------------------------------------------------------------------------------- /app/app.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo Starting container and build the application... 4 | 5 | docker run -d -p 8080:8080 --name sage_accounting_csharp_sample --volume="`pwd`/app:/application/app" sage_accounting_csharp_sample /bin/bash -c "dotnet publish" 6 | docker logs -f sage_accounting_csharp_sample 7 | -------------------------------------------------------------------------------- /script/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo Starting container and build and launch the application... 4 | 5 | docker run -d -p 8080:8080 --name sage_accounting_csharp_sample --volume="`pwd`/app:/application/app" sage_accounting_csharp_sample /bin/bash -c "dotnet publish && dotnet run" 6 | docker logs -f sage_accounting_csharp_sample 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Created by https://www.gitignore.io/api/dotnetcore 4 | # Edit at https://www.gitignore.io/?templates=dotnetcore 5 | 6 | ### DotnetCore ### 7 | # .NET Core build folders 8 | /app/bin/ 9 | /app/obj/ 10 | /app/out/ 11 | 12 | # exclude the used client config file 13 | /app/client_application.json 14 | /app/access_token.json 15 | 16 | # exclude visual studio settings folder 17 | .vs 18 | 19 | app/.vscode/launch.json 20 | app/.vscode/tasks.json 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet build", 9 | "type": "shell", 10 | "group": "build", 11 | "presentation": { 12 | "reveal": "silent" 13 | }, 14 | "problemMatcher": "$msCompile" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /app/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Sage Accounting API Sample App - C# 5 | 6 | 7 | 8 | Sage Accounting API Sample App - C# 9 | 10 | 11 | 12 | 13 | @RenderBody() 14 | @RenderSection("Scripts", required: false) 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/Models/ContentModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore; 4 | 5 | namespace app.Models 6 | { 7 | public class ContentModel 8 | { 9 | public string guideBaseUrl {get; set;} 10 | public string reqAccessToken { get; set; } 11 | public string reqMethod { get; set; } 12 | public string reqEndPoint { get; set; } 13 | public string reqBody { get; set; } 14 | public string respStatusCode { get; set; } 15 | public string respBody { get; set; } 16 | 17 | public string partialAccessTokenAvailable {get; set;} 18 | public string partialResposeIsAvailable {get; set;} 19 | public string clientApplicationConfigNotFound {get; set;} 20 | } 21 | } -------------------------------------------------------------------------------- /app/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.StaticFiles; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace app 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) 17 | { 18 | CreateWebHostBuilder(args) 19 | .UseUrls("http://0.0.0.0:8080/") 20 | .Build() 21 | .Run(); 22 | } 23 | 24 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 25 | WebHost.CreateDefaultBuilder(args) 26 | .UseStartup(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "app", "app\app.csproj", "{98264946-9992-4B4E-9B36-381FEDD4E308}" 5 | EndProject 6 | Global 7 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 8 | Debug|Any CPU = Debug|Any CPU 9 | Release|Any CPU = Release|Any CPU 10 | EndGlobalSection 11 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 12 | {98264946-9992-4B4E-9B36-381FEDD4E308}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 13 | {98264946-9992-4B4E-9B36-381FEDD4E308}.Debug|Any CPU.Build.0 = Debug|Any CPU 14 | {98264946-9992-4B4E-9B36-381FEDD4E308}.Release|Any CPU.ActiveCfg = Release|Any CPU 15 | {98264946-9992-4B4E-9B36-381FEDD4E308}.Release|Any CPU.Build.0 = Release|Any CPU 16 | EndGlobalSection 17 | EndGlobal 18 | -------------------------------------------------------------------------------- /app/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:46551", 8 | "sslPort": 44369 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "app": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "api/values", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "http://localhost:8080" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Sage 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | -------------------------------------------------------------------------------- /app/Views/Home/Resp.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | @using app.Models 3 | @model ContentModel 4 | @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor 5 | @{ 6 | ViewData["Title"] = "Response"; 7 | } 8 | 9 | @if (Model.partialAccessTokenAvailable == "1" && Model.partialResposeIsAvailable == "1" && Model.clientApplicationConfigNotFound == "0") { 10 | 11 |
12 |
13 | 14 |

15 | Returned in @HttpContextAccessor.HttpContext.Session.GetString("responseTimespan") seconds 16 |

17 |
18 | 19 |
20 |
21 | 22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sage Business Cloud Accounting API Sample application (C#) [![Travis Status](https://travis-ci.org/Sage/sageone_api_csharp_sample.svg?branch=master)](https://travis-ci.org/github/Sage/sageone_api_csharp_sample) 2 | 3 | Sample C# project that integrates with Sage Accounting via the Sage Accounting API. This Application uses .NET Core 2.2 and [Newtonsoft Json.NET](https://github.com/JamesNK/Newtonsoft.Json). 4 | 5 | * Authentication and API calls are handled in [app/Startup.cs](app/Startup.cs) 6 | 7 | ## Setup 8 | 9 | Clone the repo: 10 | 11 | `git clone git@github.com:Sage/sageone_api_csharp_sample.git` 12 | 13 | Switch to the project directory to run the subsequent commands: 14 | 15 | ``` 16 | cd sageone_api_csharp_sample 17 | ``` 18 | 19 | ## Run the app locally 20 | 21 | switch to the app folder: 22 | 23 | ``` 24 | cd app 25 | ``` 26 | 27 | install all dependencies: 28 | 29 | ``` 30 | dotnet restore 31 | ``` 32 | 33 | build and run the application: 34 | 35 | ``` 36 | dotnet build 37 | dotnet run 38 | ``` 39 | 40 | Then jump to the section [Usage](#Usage). 41 | 42 | ## Run the app in Docker 43 | 44 | Build the image: 45 | 46 | ``` 47 | ./script/setup.sh 48 | ``` 49 | 50 | Start the container: 51 | 52 | ``` 53 | ./script/start.sh 54 | ``` 55 | 56 | Restart the container: 57 | 58 | ``` 59 | ./script/restart.sh 60 | ``` 61 | 62 | If you need, stop.sh will stop the container: 63 | 64 | ``` 65 | ./script/stop.sh 66 | ``` 67 | 68 | ## Usage 69 | 70 | You can now access [http://localhost:8080/](http://localhost:8080/), authorize and make an API call. Depending on your setup, it could also be [http://192.168.99.100:8080/](http://192.168.99.100:8080/) or similar. 71 | 72 | ## License 73 | 74 | This sample application is available as open source under the terms of the 75 | [MIT licence](LICENSE). 76 | 77 | Copyright (c) 2019 Sage Group Plc. All rights reserved. 78 | -------------------------------------------------------------------------------- /app/Views/Home/Guide.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | @model app.Models.ContentModel 3 | @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor 4 | @{ 5 | ViewData["Title"] = "Guide"; 6 | } 7 |
8 |

Let's get started

9 | 10 |
    11 |
  1. 12 |

    13 | Sign up for a Sage developer account 14 |

    15 |
  2. 16 |
  3. 17 |

    18 | Create a client application 19 |

    20 |
    21 | Enter the following as the callback URL:
    22 | @HttpContextAccessor.HttpContext.Session.GetString("BaseUrl")/auth/callback 23 |
    24 |
  4. 25 |
  5. 26 |

    27 | Copy the Client Id and Client Secret into: 28 |

    29 |
    30 | app/client_application.json 31 |
    32 |
  6. 33 |
  7. 34 |

    35 | Sign up for a free product trial 36 |

    37 |
  8. 38 |
  9. 39 |

    40 | Authorize API access
    41 |

    42 |
  10. 43 |
  11. 44 |

    45 | Make your first API call 46 |

    47 |
  12. 48 |
49 |
50 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": ".NET Core Launch (console)", 10 | "type": "coreclr", 11 | "request": "launch", 12 | "WARNING01": "*********************************************************************************", 13 | "WARNING02": "The C# extension was unable to automatically decode projects in the current", 14 | "WARNING03": "workspace to create a runnable launch.json file. A template launch.json file has", 15 | "WARNING04": "been created as a placeholder.", 16 | "WARNING05": "", 17 | "WARNING06": "If OmniSharp is currently unable to load your project, you can attempt to resolve", 18 | "WARNING07": "this by restoring any missing project dependencies (example: run 'dotnet restore')", 19 | "WARNING08": "and by fixing any reported errors from building the projects in your workspace.", 20 | "WARNING09": "If this allows OmniSharp to now load your project then --", 21 | "WARNING10": " * Delete this file", 22 | "WARNING11": " * Open the Visual Studio Code command palette (View->Command Palette)", 23 | "WARNING12": " * run the command: '.NET: Generate Assets for Build and Debug'.", 24 | "WARNING13": "", 25 | "WARNING14": "If your project requires a more complex launch configuration, you may wish to delete", 26 | "WARNING15": "this configuration and pick a different template using the 'Add Configuration...'", 27 | "WARNING16": "button at the bottom of this file.", 28 | "WARNING17": "*********************************************************************************", 29 | "preLaunchTask": "build", 30 | "program": "${workspaceFolder}/app/bin/Debug/netcoreapp2.2/app.dll", 31 | "args": [], 32 | "cwd": "${workspaceFolder}", 33 | "console": "internalConsole", 34 | "stopAtEntry": false 35 | }, 36 | { 37 | "name": ".NET Core Attach", 38 | "type": "coreclr", 39 | "request": "attach", 40 | "processId": "${command:pickProcess}" 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /app/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Http; 5 | using System.Text.Json; 6 | using app.Models; 7 | 8 | namespace app.Controllers 9 | { 10 | public class HomeController : Controller 11 | { 12 | ContentModel model = new ContentModel(); 13 | 14 | public IActionResult Index() 15 | { 16 | // if access_token.json exists load it and set view settings 17 | this.tokenfileRead(HttpContext); 18 | 19 | HttpContext.Session.SetString("BaseUrl", "http://" + HttpContext.Request.Host); 20 | 21 | if (Startup.getPathOfConfigFile().Equals("")) 22 | { 23 | // show warning on Req.cshtml 24 | model.clientApplicationConfigNotFound = "1"; 25 | } 26 | else 27 | { 28 | model.clientApplicationConfigNotFound = "0"; 29 | } 30 | 31 | var readValue = new Byte[1024]; 32 | if (!HttpContext.Session.TryGetValue("reqEndpoint", out readValue)) 33 | HttpContext.Session.SetString("reqEndpoint", "contacts"); 34 | 35 | 36 | if (!HttpContext.Session.TryGetValue("access_token", out readValue)) 37 | { 38 | // access_token field is empty 39 | model.partialAccessTokenAvailable = "0"; 40 | model.partialResposeIsAvailable = "0"; 41 | } 42 | else if (HttpContext.Session.TryGetValue("access_token", out readValue) && !HttpContext.Session.TryGetValue("responseContent", out readValue)) 43 | { 44 | // access_token filled and responseContent is empty 45 | model.partialAccessTokenAvailable = "1"; 46 | model.partialResposeIsAvailable = "0"; 47 | } 48 | else 49 | { 50 | model.partialAccessTokenAvailable = "1"; 51 | model.partialResposeIsAvailable = "1"; 52 | } 53 | 54 | return View(model); 55 | } 56 | 57 | public IActionResult Guide() 58 | { 59 | return View(); 60 | } 61 | 62 | public IActionResult Req() 63 | { 64 | return View(); 65 | } 66 | 67 | public IActionResult Resp() 68 | { 69 | return View(); 70 | } 71 | 72 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 73 | public IActionResult Error() 74 | { 75 | return View(model); 76 | } 77 | 78 | public void tokenfileRead(HttpContext context) 79 | { 80 | if (System.IO.File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "access_token.json"))) 81 | { 82 | String fs = System.IO.File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "access_token.json")); 83 | 84 | var content = JsonSerializer.Deserialize(fs); 85 | context.Request.HttpContext.Session.SetString("access_token", content.access_token); 86 | context.Request.HttpContext.Session.SetString("expires_at", content.expires_at.ToString()); 87 | context.Request.HttpContext.Session.SetString("refresh_token", content.refresh_token); 88 | context.Request.HttpContext.Session.SetString("refresh_token_expires_at", content.refresh_token_expires_at.ToString()); 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/Views/Home/Req.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | @model app.Models.ContentModel 3 | @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor 4 | @{ 5 | ViewData["Title"] = "Request"; 6 | } 7 | 8 | @if (Model.partialAccessTokenAvailable == "1" && Model.clientApplicationConfigNotFound == "0") { 9 | 10 |
11 | 12 |
13 | 14 |

...

15 |
16 | 17 | Refresh Token 18 |
19 |
20 | 21 | 51 | 52 |
53 | 54 |
55 | 61 |
62 |
63 | 64 |
65 | 66 |

67 | https://api.accounting.sage.com/v3.1/<Resource> 68 |

69 |
70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 | 78 |
79 | 80 |

81 | Example: {"contact": { "contact_type_ids": ["CUSTOMER"], "name": "Joe Bloggs"}} 82 |

83 |
84 | 85 |
86 |
87 | 88 | 89 |
90 | 91 | 92 | } 93 | else if (Model.partialAccessTokenAvailable == "0" && Model.partialResposeIsAvailable == "0" && Model.clientApplicationConfigNotFound == "0") { 94 | 95 | Authorize API access 96 | 97 | } 98 | else if (Model.clientApplicationConfigNotFound == "1" && Model.partialAccessTokenAvailable == "0" && Model.partialResposeIsAvailable == "0") { 99 |
100 |

There was a problem loading client_application.json

101 |

Before you can start, you need to prepare a config file, which contains information 102 | about your registered client application.

103 |

Follow these steps to get this sample application running:

104 |
    105 |
  • Make a copy of the file app/client_application.template.json and 106 | name it app/client_application.json.
  • 107 |
  • Go to the Sage Developer Self Service at 108 | https://developerselfservice.sageone.com/ 109 | and sign up or sign in.
  • 110 |
  • Create a new application (app) and copy the values for Client ID and Client Secret 111 | into the fields in client_application.json.
  • 112 |
  • Depending on the way you run the sample app (Docker environment, local Apache, 113 | etc.) adjust the hostname/port of the callback URL below. Add the same callback URL 114 | to your client application in the Sage Developer Self Service.
  • 115 |
  • You will then need to restart the application in order to use the updated 116 | credentials and callback URL in app/client_application.json
  • 117 |
118 |
119 | } 120 | -------------------------------------------------------------------------------- /app/wwwroot/sample_app.css: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background: #f2f5f6; 9 | font: 87.5%/1.5 "Open Sans", sans-serif; 10 | min-height: 100vh; 11 | display: flex; 12 | flex-grow: 1; 13 | margin: 0; 14 | } 15 | 16 | a:link:not(.button), 17 | a:visited:not(.button) { 18 | text-decoration: none; 19 | color: #008200; 20 | } 21 | 22 | a:hover:not(.button) { 23 | color: #006800; 24 | } 25 | 26 | a:active:not(.button) { 27 | color: #004500; 28 | } 29 | 30 | .guide { 31 | background: #fff; 32 | max-width: 432px; 33 | min-width: 320px; 34 | overflow: auto; 35 | height: 100vh; 36 | padding: 40px; 37 | flex: 1 432px; 38 | } 39 | 40 | .guide:target { 41 | display: block; 42 | } 43 | 44 | .guide::before { 45 | background: url(sage-developers.png) no-repeat 50% 0 / contain; 46 | padding: 0 0 calc((479 / 864) * (100% + 80px)); 47 | margin: 0 -40px 40px; 48 | display: block; 49 | content: ""; 50 | } 51 | 52 | .title { 53 | color: rgba(0, 0, 0, 0.9); 54 | font-weight: bold; 55 | line-height: 30px; 56 | margin: 0 0 24px; 57 | font-size: 24px; 58 | } 59 | 60 | .steps { 61 | padding: 0; 62 | margin: 0; 63 | } 64 | 65 | .step { 66 | counter-increment: item; 67 | list-style: none inside; 68 | margin-bottom: 10px; 69 | padding-left: 32px; 70 | position: relative; 71 | margin: 20px 0 0; 72 | } 73 | 74 | .step__title { 75 | font-size: inherit; 76 | font-weight: 600; 77 | margin: 0 0 4px; 78 | } 79 | 80 | .step__body { 81 | color: rgba(0, 0, 0, 0.55); 82 | } 83 | 84 | .step::before { 85 | content: " " counter(item) " "; 86 | border: 1px solid #99adb6; 87 | justify-content: center; 88 | display: inline-flex; 89 | align-items: center; 90 | text-align: center; 91 | position: absolute; 92 | border-radius: 50%; 93 | font-weight: bold; 94 | font-size: 13px; 95 | height: 24px; 96 | width: 24px; 97 | left: 0; 98 | top: 0; 99 | } 100 | 101 | .main { 102 | padding: 0 20px; 103 | flex-wrap: wrap; 104 | overflow: auto; 105 | height: 100vh; 106 | display: flex; 107 | flex: 999; 108 | } 109 | 110 | .request { 111 | padding: 40px 20px; 112 | min-width: 432px; 113 | flex: 1 50%; 114 | } 115 | 116 | .response { 117 | padding: 40px 20px; 118 | min-width: 432px; 119 | flex: 999 432px; 120 | } 121 | 122 | .field:not(:last-child) { 123 | margin-bottom: 24px; 124 | } 125 | 126 | .field__label { 127 | margin-bottom: 8px; 128 | line-height: 21px; 129 | font-weight: 600; 130 | display: block; 131 | } 132 | 133 | .field__help { 134 | color: rgba(0, 0, 0, 0.55); 135 | margin-bottom: 4px; 136 | position: relative; 137 | top: -8px; 138 | } 139 | 140 | .field__help.is-error { 141 | color: #C7384F; 142 | } 143 | 144 | .field__body { 145 | display: flex; 146 | } 147 | 148 | .field__input { 149 | text-overflow: ellipsis; 150 | line-height: inherit; 151 | position: relative; 152 | overflow: hidden; 153 | font: inherit; 154 | flex: 1 1; 155 | 156 | border: 1px solid #668592; 157 | -webkit-appearance: none; 158 | -moz-appearance: none; 159 | background: #fff; 160 | appearance: none; 161 | border-radius: 0; 162 | resize: vertical; 163 | font-size: 16px; 164 | padding: 8px; 165 | height: 40px; 166 | } 167 | 168 | /* .field__input:read-only:not(select) { 169 | background: rgba(255, 255, 255, 0.5); 170 | border-color: rgba(0, 0, 0, 0.15); 171 | } */ 172 | 173 | .field__input:disabled { 174 | border-color: rgba(0, 0, 0, 0.15); 175 | color: rgba(0,0,0,.55); 176 | background: transparent; 177 | cursor: not-allowed; 178 | } 179 | 180 | .field__input.is-invalid { 181 | border: 2px solid #C7384F; 182 | } 183 | 184 | .field__input:focus:not(:disabled), 185 | .button:focus:not(:disabled) { 186 | outline: 3px solid #ffb500; 187 | outline-offset: 0; 188 | z-index: 1; 189 | } 190 | 191 | .button { 192 | text-decoration: none; 193 | background: #008200; 194 | display: inline-block; 195 | white-space: nowrap; 196 | position: relative; 197 | font-weight: bold; 198 | line-height:40px; 199 | cursor: pointer; 200 | font-size: 16px; 201 | padding: 0 24px; 202 | color: #fff; 203 | height: 40px; 204 | border: 0; 205 | margin: 0; 206 | } 207 | 208 | .button:hover { 209 | background: #006800; 210 | } 211 | 212 | .button:active { 213 | background: #004500; 214 | } 215 | .button:disabled { 216 | background-color: #e6ebed; 217 | color: rgba(0,0,0,.3); 218 | cursor: not-allowed; 219 | } 220 | 221 | .button.is-destructive { 222 | background: #c7384f; 223 | } 224 | 225 | .button.is-centered { 226 | margin: auto; 227 | } 228 | 229 | .form__button { 230 | margin-top: 48px; 231 | } 232 | 233 | .response__body { 234 | font-family: monospace; 235 | font-size: 14px; 236 | white-space: pre; 237 | overflow: auto; 238 | height: calc(100vh - 234px); 239 | } 240 | 241 | code { 242 | border: 1px solid rgba(0, 0, 0, 0.15); 243 | color: rgba(0, 0, 0, 0.95); 244 | font-family: monospace; 245 | background: #f4f4f4; 246 | border-radius: 4px; 247 | padding: 2px 4px; 248 | font-size: 12px; 249 | } 250 | 251 | /* https://github.com/h5bp/html5-boilerplate/blob/5f00e406721bac39dcd7b87ca185ad02b2dcb1c4/dist/css/main.css#L122 */ 252 | .sr-only { 253 | border: 0; 254 | clip: rect(0, 0, 0, 0); 255 | height: 1px; 256 | margin: -1px; 257 | overflow: hidden; 258 | padding: 0; 259 | position: absolute; 260 | white-space: nowrap; 261 | width: 1px; 262 | /* 1 */ 263 | } 264 | 265 | .products { 266 | list-style: none; 267 | padding: 0; 268 | margin: 0; 269 | } 270 | 271 | .product { 272 | line-height: 24px; 273 | height: 24px; 274 | margin: 0; 275 | } 276 | 277 | .flag::before { 278 | vertical-align: middle; 279 | font-size: 24px; 280 | } 281 | 282 | .flag--ca::before { 283 | content: "🇨🇦 "; 284 | } 285 | 286 | .flag--de::before { 287 | content: "🇩🇪 "; 288 | } 289 | 290 | .flag--es::before { 291 | content: "🇪🇦 "; 292 | } 293 | 294 | .flag--fr::before { 295 | content: "🇫🇷 "; 296 | } 297 | 298 | .flag--gb::before { 299 | content: "🇬🇧 "; 300 | } 301 | 302 | .flag--ie::before { 303 | content: "🇮🇪 "; 304 | } 305 | 306 | .flag--us::before { 307 | content: "🇺🇸 "; 308 | } -------------------------------------------------------------------------------- /app/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text.Encodings.Web; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Net.Http.Headers; 10 | using Microsoft.AspNetCore.Authentication; 11 | using Microsoft.AspNetCore.Authentication.Cookies; 12 | using Microsoft.AspNetCore.Authentication.OAuth; 13 | using Microsoft.AspNetCore.Builder; 14 | using Microsoft.AspNetCore.Hosting; 15 | using Microsoft.AspNetCore.Http; 16 | using Microsoft.Extensions.Configuration; 17 | using Microsoft.Extensions.DependencyInjection; 18 | using Microsoft.Extensions.Hosting; 19 | using Microsoft.Extensions.Options; 20 | using System.Text.Json; 21 | using System.IO; 22 | 23 | namespace app 24 | { 25 | public class JsonResponse 26 | { 27 | public String access_token { get; set; } 28 | public long expires_in { get; set; } 29 | public String bearer { get; set; } 30 | public String refresh_token { get; set; } 31 | public long refresh_token_expires_in { get; set; } 32 | public String scope { get; set; } 33 | public String requested_by_id { get; set; } 34 | } 35 | public class JsonAccessTokenFile 36 | { 37 | public String access_token { get; set; } 38 | public long expires_at { get; set; } 39 | public String refresh_token { get; set; } 40 | public long refresh_token_expires_at { get; set; } 41 | } 42 | public class JsonClientApplicationFile 43 | { 44 | public JsonClientApplicationFileConfigSection config { get; set; } 45 | } 46 | public class JsonClientApplicationFileConfigSection 47 | { 48 | public String client_id { get; set; } 49 | public String client_secret { get; set; } 50 | public String callback_url { get; set; } 51 | } 52 | public class Startup 53 | { 54 | public Startup(IConfiguration configuration) 55 | { 56 | Configuration = configuration; 57 | } 58 | 59 | public IConfiguration Configuration { get; } 60 | const string API_URL = "https://api.accounting.sage.com/v3.1/"; 61 | const string AUTHORIZATION_ENDPOINT = "https://www.sageone.com/oauth2/auth/central?filter=apiv3.1"; 62 | const string TOKEN_ENDPOINT = "https://oauth.accounting.sage.com/token"; 63 | 64 | // This method gets called by the runtime. Use this method to add services to the container. 65 | public void ConfigureServices(IServiceCollection services) 66 | { 67 | 68 | String config_client_id = "initial"; 69 | String config_client_secret = "initial"; 70 | String config_calback_url = "initial"; 71 | 72 | if (!(getPathOfConfigFile().Equals(""))) 73 | { 74 | String fs = File.ReadAllText(getPathOfConfigFile()); 75 | { 76 | var content = System.Text.Json.JsonSerializer.Deserialize(fs); 77 | config_client_id = content.config.client_id; 78 | config_client_secret = content.config.client_secret; 79 | config_calback_url = content.config.callback_url; 80 | } 81 | } 82 | 83 | services.AddDistributedMemoryCache(); 84 | services.AddSession(options => 85 | { 86 | options.Cookie.HttpOnly = false; 87 | options.Cookie.IsEssential = true; 88 | options.IdleTimeout = TimeSpan.FromHours(1); 89 | }); 90 | 91 | services.AddSingleton(); 92 | services.AddRazorPages(); 93 | services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) 94 | .AddCookie(o => o.LoginPath = new PathString("/login")) 95 | .AddOAuth("oauth2", "Sage Accounting", o => 96 | { 97 | o.ClientId = config_client_id; 98 | o.ClientSecret = config_client_secret; 99 | o.CallbackPath = new PathString("/auth/callback"); 100 | o.AuthorizationEndpoint = AUTHORIZATION_ENDPOINT; 101 | o.TokenEndpoint = TOKEN_ENDPOINT; 102 | o.SaveTokens = true; 103 | 104 | o.Scope.Add("full_access"); 105 | o.Events = new OAuthEvents 106 | { 107 | OnRemoteFailure = HandleOnRemoteFailure, 108 | OnCreatingTicket = async context => 109 | { 110 | long tok_expires_in = context.TokenResponse.Response.RootElement.GetProperty("expires_in").GetInt64(); 111 | long tok_refresh_token_expires_in = context.TokenResponse.Response.RootElement.GetProperty("refresh_token_expires_in").GetInt64(); 112 | 113 | tokenfileWrite(context.AccessToken, 114 | calculateUnixtimestampWithOffset(tok_expires_in), 115 | context.RefreshToken, 116 | calculateUnixtimestampWithOffset(tok_refresh_token_expires_in), 117 | context.HttpContext); 118 | return; 119 | } 120 | }; 121 | }); 122 | } 123 | 124 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 125 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 126 | { 127 | if (env.IsDevelopment()) 128 | { 129 | app.UseDeveloperExceptionPage(); 130 | } 131 | else 132 | { 133 | app.UseExceptionHandler("/Home/Error"); 134 | app.UseHsts(); 135 | } 136 | app.UseRouting(); 137 | app.UseStaticFiles(); 138 | app.UseSession(); 139 | app.UseCookiePolicy(); 140 | app.UseAuthentication(); 141 | app.UseEndpoints(endpoints => endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}")); 142 | 143 | // get access token 144 | app.Map("/login", signinApp => 145 | { 146 | signinApp.Run(async context => 147 | { 148 | await context.ChallengeAsync("oauth2", new AuthenticationProperties() { RedirectUri = "/" }); 149 | 150 | return; 151 | }); 152 | }); 153 | 154 | // Refresh the access token 155 | app.Map("/refresh_token", signinApp => 156 | { 157 | signinApp.Run(async context => 158 | { 159 | var response = context.Response; 160 | 161 | // This is what [Authorize] calls 162 | var userResult = await context.AuthenticateAsync(); 163 | var user = userResult.Principal; 164 | var authProperties = userResult.Properties; 165 | 166 | // Deny anonymous request beyond this point. 167 | if (!userResult.Succeeded || user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) 168 | { 169 | // This is what [Authorize] calls 170 | // The cookie middleware will handle this and redirect to /login 171 | await context.ChallengeAsync(); 172 | return; 173 | } 174 | 175 | var currentAuthType = user.Identities.First().AuthenticationType; 176 | var refreshToken = authProperties.GetTokenValue("refresh_token"); 177 | 178 | if (string.IsNullOrEmpty(refreshToken)) 179 | { 180 | response.ContentType = "text/html"; 181 | await response.WriteAsync(""); 182 | await response.WriteAsync("No refresh_token is available.
"); 183 | await response.WriteAsync("Home"); 184 | await response.WriteAsync(""); 185 | return; 186 | } 187 | 188 | var options = await GetOAuthOptionsAsync(context, currentAuthType); 189 | 190 | var pairs = new Dictionary() 191 | { 192 | { "client_id", options.ClientId }, 193 | { "client_secret", options.ClientSecret }, 194 | { "grant_type", "refresh_token" }, 195 | { "refresh_token", refreshToken } 196 | }; 197 | var content = new FormUrlEncodedContent(pairs); 198 | var refreshResponse = await options.Backchannel.PostAsync(options.TokenEndpoint, content, context.RequestAborted); 199 | refreshResponse.EnsureSuccessStatusCode(); 200 | 201 | var jsonResponse = JsonSerializer.Deserialize((string)await refreshResponse.Content.ReadAsStringAsync()); 202 | 203 | authProperties.UpdateTokenValue("access_token", jsonResponse.access_token); 204 | authProperties.UpdateTokenValue("refresh_token", jsonResponse.refresh_token); 205 | 206 | var expiresAt = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(jsonResponse.expires_in); 207 | authProperties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture)); 208 | 209 | await context.SignInAsync(user, authProperties); 210 | 211 | // write new tokens and times to file 212 | tokenfileWrite(jsonResponse.access_token, 213 | calculateUnixtimestampWithOffset(jsonResponse.expires_in), 214 | jsonResponse.refresh_token, 215 | calculateUnixtimestampWithOffset(jsonResponse.refresh_token_expires_in), 216 | context); 217 | 218 | context.Response.Redirect("/"); 219 | 220 | return; 221 | }); 222 | }); 223 | 224 | app.Map("/query_api", signinApp => 225 | { 226 | signinApp.Run(async context => 227 | { 228 | String qry_http_verb = context.Request.Query["http_verb"].ToString() ?? ""; 229 | String qry_resource = context.Request.Query["resource"].ToString() ?? ""; 230 | String qry_post_data = context.Request.Query["post_data"].ToString() ?? ""; 231 | 232 | System.Diagnostics.Stopwatch timer = new System.Diagnostics.Stopwatch(); 233 | timer.Start(); 234 | 235 | using (HttpClient client = new HttpClient()) 236 | { 237 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", context.Response.HttpContext.Session.GetString("access_token")); 238 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 239 | 240 | HttpResponseMessage request = null; 241 | 242 | if (qry_http_verb.Equals("get")) 243 | { 244 | request = await client.GetAsync(API_URL + qry_resource); 245 | } 246 | else if (qry_http_verb.Equals("post")) 247 | { 248 | request = await client.PostAsync(API_URL + qry_resource, new StringContent(qry_post_data, Encoding.UTF8, "application/json")); 249 | } 250 | else if (qry_http_verb.Equals("put")) 251 | { 252 | request = await client.PutAsync(API_URL + qry_resource, new StringContent(qry_post_data, Encoding.UTF8, "application/json")); 253 | } 254 | else if (qry_http_verb.Equals("delete")) 255 | { 256 | request = await client.DeleteAsync(API_URL + qry_resource); 257 | } 258 | 259 | Task respContent = request.Content.ReadAsStringAsync(); 260 | Task.WaitAll(respContent); 261 | 262 | dynamic parsedJson = Newtonsoft.Json.JsonConvert.DeserializeObject(respContent.Result.ToString()); 263 | String responseContentPretty = Newtonsoft.Json.JsonConvert.SerializeObject(parsedJson, Newtonsoft.Json.Formatting.Indented); 264 | 265 | context.Response.HttpContext.Session.SetString("responseStatusCode", (int)request.StatusCode + " - " + request.StatusCode.ToString()); 266 | context.Response.HttpContext.Session.SetString("reqEndpoint", qry_resource); 267 | 268 | context.Response.HttpContext.Session.SetString("responseContent", responseContentPretty); 269 | context.Response.HttpContext.Session.SetString("responseTimespan", timer.Elapsed.Seconds + "." + timer.Elapsed.Milliseconds); 270 | 271 | } 272 | 273 | context.Response.Redirect("/"); 274 | return; 275 | }); 276 | }); 277 | 278 | app.Run(async context => 279 | { 280 | tokenfileRead(context); 281 | 282 | // Setting DefaultAuthenticateScheme causes User to be set 283 | var user = context.User; 284 | 285 | // Deny anonymous request beyond this point. 286 | if (user == null || !user.Identities.Any(identity => identity.IsAuthenticated)) 287 | { 288 | // This is what [Authorize] calls 289 | // The cookie middleware will handle this and redirect to /login 290 | await context.ChallengeAsync(); 291 | 292 | return; 293 | } 294 | }); 295 | 296 | // Sign-out to remove the user cookie. 297 | app.Map("/logout", signoutApp => 298 | { 299 | signoutApp.Run(async context => 300 | { 301 | var response = context.Response; 302 | response.ContentType = "text/html"; 303 | await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 304 | await response.WriteAsync(""); 305 | await response.WriteAsync("You have been logged out. Goodbye " + context.User.Identity.Name + "
"); 306 | await response.WriteAsync("Home"); 307 | await response.WriteAsync(""); 308 | }); 309 | }); 310 | 311 | // Display the remote error 312 | app.Map("/error", errorApp => 313 | { 314 | errorApp.Run(async context => 315 | { 316 | var response = context.Response; 317 | response.ContentType = "text/html"; 318 | await response.WriteAsync(""); 319 | await response.WriteAsync("An remote failure has occurred: " + context.Request.Query["FailureMessage"] + "
"); 320 | await response.WriteAsync("Home"); 321 | await response.WriteAsync(""); 322 | }); 323 | }); 324 | } 325 | 326 | #region helper 327 | private async Task HandleOnRemoteFailure(RemoteFailureContext context) 328 | { 329 | context.Response.StatusCode = 500; 330 | context.Response.ContentType = "text/html"; 331 | await context.Response.WriteAsync(""); 332 | await context.Response.WriteAsync("A remote failure has occurred:
" + 333 | context.Failure.Message.Split(Environment.NewLine).Select(s => HtmlEncoder.Default.Encode(s) + "
").Aggregate((s1, s2) => s1 + s2)); 334 | 335 | if (context.Properties != null) 336 | { 337 | await context.Response.WriteAsync("Properties:
"); 338 | foreach (var pair in context.Properties.Items) 339 | { 340 | await context.Response.WriteAsync($"-{ HtmlEncoder.Default.Encode(pair.Key)}={ HtmlEncoder.Default.Encode(pair.Value)}
"); 341 | } 342 | } 343 | 344 | await context.Response.WriteAsync("Home"); 345 | await context.Response.WriteAsync(""); 346 | 347 | context.HandleResponse(); 348 | } 349 | 350 | private Task GetOAuthOptionsAsync(HttpContext context, string currentAuthType) 351 | { 352 | return Task.FromResult(context.RequestServices.GetRequiredService>().Get(currentAuthType)); 353 | } 354 | 355 | public string tokenfileWrite(string access_token, long expires_at, string refresh_token, long refresh_token_expires_at, HttpContext context) 356 | { 357 | JsonAccessTokenFile content = new JsonAccessTokenFile(); 358 | 359 | content.access_token = access_token; 360 | content.expires_at = expires_at; 361 | content.refresh_token = refresh_token; 362 | content.refresh_token_expires_at = refresh_token_expires_at; 363 | 364 | var options = new JsonSerializerOptions { WriteIndented = true }; 365 | 366 | var newContent = JsonSerializer.Serialize(content, options); 367 | 368 | File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), "access_token.json"), newContent.ToString()); 369 | 370 | context.Request.HttpContext.Session.SetString("access_token", access_token); 371 | context.Request.HttpContext.Session.SetString("expires_at", expires_at.ToString()); 372 | context.Request.HttpContext.Session.SetString("refresh_token", refresh_token); 373 | context.Request.HttpContext.Session.SetString("refresh_token_expires_at", refresh_token_expires_at.ToString()); 374 | 375 | return "0"; 376 | } 377 | 378 | public static Dictionary tokenfileRead(HttpContext context) 379 | { 380 | Dictionary contentFromFile = new Dictionary(); 381 | 382 | if (File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "access_token.json"))) 383 | { 384 | 385 | String fs = File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "access_token.json")); 386 | 387 | var content = JsonSerializer.Deserialize(fs); 388 | context.Request.HttpContext.Session.SetString("access_token", content.access_token); 389 | contentFromFile.Add("access_token", content.access_token); 390 | 391 | context.Request.HttpContext.Session.SetString("expires_at", content.expires_at.ToString()); 392 | contentFromFile.Add("expires_at", content.expires_at.ToString()); 393 | 394 | context.Request.HttpContext.Session.SetString("refresh_token", content.refresh_token); 395 | contentFromFile.Add("refresh_token", content.refresh_token); 396 | 397 | context.Request.HttpContext.Session.SetString("refresh_token_expires_at", content.refresh_token_expires_at.ToString()); 398 | contentFromFile.Add("refresh_token_expires_at", content.refresh_token_expires_at.ToString()); 399 | } 400 | return contentFromFile; 401 | } 402 | 403 | public static long calculateUnixtimestampWithOffset(long offset = 0) 404 | { 405 | long seconds = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0)).TotalSeconds + offset; 406 | 407 | return seconds; 408 | } 409 | 410 | public static String getPathOfConfigFile() 411 | { 412 | 413 | if (System.IO.File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "client_application.json"))) 414 | { 415 | return Path.Combine(Directory.GetCurrentDirectory(), "client_application.json"); 416 | } 417 | else if (System.IO.File.Exists(Path.Combine(Directory.GetCurrentDirectory(), "app/client_application.json"))) 418 | { 419 | return Path.Combine(Directory.GetCurrentDirectory(), "app/client_application.json"); 420 | } 421 | 422 | return ""; 423 | } 424 | } 425 | #endregion 426 | } 427 | --------------------------------------------------------------------------------