├── 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 |
Response Status
14 |
15 | Returned in @HttpContextAccessor.HttpContext.Session.GetString("responseTimespan") seconds
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Response Body
24 |
25 |
26 |
27 |
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Sage Business Cloud Accounting API Sample application (C#) [](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 |
12 |
15 |
16 |
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 |
25 |
26 |
27 | Copy the Client Id and Client Secret into:
28 |
29 |
30 | app/client_application.json
31 |
32 |
33 |
34 |
37 |
38 |
39 |
42 |
43 |
44 |
45 | Make your first API call
46 |
47 |
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 |
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 |
--------------------------------------------------------------------------------