This commit is contained in:
Simon 2021-10-02 01:43:52 +01:00
parent 1b4c4add09
commit a6f0f8c72a
16 changed files with 496 additions and 35 deletions

View File

@ -5,4 +5,5 @@ go 1.15
require ( require (
github.com/gin-gonic/gin v1.7.4 github.com/gin-gonic/gin v1.7.4
go.mongodb.org/mongo-driver v1.7.2 go.mongodb.org/mongo-driver v1.7.2
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
) )

View File

@ -19,6 +19,10 @@ func main() {
func initializeRoutes(r *gin.Engine) { func initializeRoutes(r *gin.Engine) {
r.POST("/data", src.CreateData) r.POST("/data", src.CreateData)
r.PUT("/data/:id", src.CreateEnvironmentData) r.PUT("/data/:id", src.CreateEnvironmentData)
r.Use(src.AEADHandler)
{
r.PUT("/data/authed/:id", src.CreateEnvironmentData)
}
} }
func getPort() string { func getPort() string {

View File

@ -2,15 +2,22 @@ package src
import ( import (
"context" "context"
"fmt"
"encoding/base64"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref" "go.mongodb.org/mongo-driver/mongo/readpref"
"golang.org/x/crypto/blake2s"
) )
const uri = "mongodb://192.168.0.159:27017" const uri = "mongodb://192.168.0.159:27017"
const salt = "ENVIRONMENT"
var dbCollection *mongo.Collection var dbCollection *mongo.Collection
var dbDevices *mongo.Collection
var mongoClient *mongo.Client var mongoClient *mongo.Client
func DbConnect() { func DbConnect() {
@ -23,8 +30,38 @@ func DbConnect() {
if err := mongoClient.Ping(context.TODO(), readpref.Primary()); err != nil { if err := mongoClient.Ping(context.TODO(), readpref.Primary()); err != nil {
panic(err) panic(err)
} }
db := mongoClient.Database("Environment")
dbCollection = db.Collection("Main")
dbDevices = db.Collection("Devices")
}
dbCollection = mongoClient.Database("Environment").Collection("Main") func GetDeviceKey(api uint64) ([]byte, error) {
apiSigned := int64(api)
filter := bson.D{{"ApiID", apiSigned}}
var result bson.M
err := dbDevices.FindOne(context.TODO(), filter).Decode(&result)
if err != nil {
return nil, err
}
key, err := DeriveKey(result["Passcode"].(string))
fmt.Printf(base64.StdEncoding.EncodeToString(key))
// We should cache this!
return key, err
}
func DeriveKey(passcode string) ([]byte, error) {
hash, err := blake2s.New256(nil)
if err != nil {
return nil, err
}
hash.Write([]byte(salt))
hash.Write([]byte(passcode))
fmt.Printf("SALT %s PASS %s\n", salt, passcode)
return hash.Sum(nil), nil
} }
func DbDisconnect() { func DbDisconnect() {

58
Api/src/middleware.go Normal file
View File

@ -0,0 +1,58 @@
package src
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/chacha20poly1305"
)
const ivSize = 12
const DecryptedData = "DecryptedData"
func AEADHandler(c *gin.Context) {
// get id
uintID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
fmt.Printf("ERR %s", err.Error())
c.AbortWithStatus(http.StatusUnauthorized)
} else {
// get key
passcode, err := GetDeviceKey(uintID)
if err != nil {
fmt.Printf("ERR %s", err.Error())
c.AbortWithStatus(http.StatusNotFound)
} else {
// get content
data, err := c.GetRawData()
if err != nil {
fmt.Printf("ERR %s", err.Error())
c.AbortWithStatus(http.StatusBadRequest)
} else {
// decrypt
iv, ciphertext := data[:ivSize], data[ivSize:]
aead, err := chacha20poly1305.New(passcode)
if err != nil {
fmt.Printf("ERR %s", err.Error())
c.AbortWithStatus(http.StatusInternalServerError)
} else {
fmt.Printf("iv: %s cypher: %s", base64.StdEncoding.EncodeToString(iv), base64.StdEncoding.EncodeToString(ciphertext))
plaintext, err := aead.Open(nil, iv, ciphertext, nil)
if err != nil {
fmt.Printf("ERR %s", err.Error())
c.AbortWithStatus(http.StatusBadRequest)
} else {
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(plaintext))
}
}
}
}
}
}

View File

@ -3,6 +3,7 @@
#include <Ticker.h> #include <Ticker.h>
#include <U8g2lib.h> #include <U8g2lib.h>
#include <ESP8266HTTPClient.h> #include <ESP8266HTTPClient.h>
#include "byte_buffer.h"
//scl D1 //scl D1
//sda D2 //sda D2
@ -27,8 +28,15 @@ float humidity = 0;
#define TEMP "Temperature: " #define TEMP "Temperature: "
#define HUMID "Humidity: " #define HUMID "Humidity: "
#define DERIVATION_HASH_SIZE 32
static constexpr const char *DERIVATION_SALT = "ENVIRONMENT";
void UpdateEnvironment(); void UpdateEnvironment();
void PostEnvironment(); void PostEnvironment();
void InitializeWifi(); void InitializeWifi();
utils::byte_buffer Encrypt(std::string data);
void DeriveKey();

View File

@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
#include <string>
namespace Environment
{
class Settings
{
private:
/* data */
public:
uint64_t apiID;
std::string endpoint;
std::string passcode;
};
}

View File

@ -26,7 +26,7 @@ class setup_server
bool serverConnection; bool serverConnection;
uint8_t connect_to_ap(const std::string ssid, const std::string password); uint8_t connect_to_ap(const std::string &ssid, const std::string &password);
void root_callback(); void root_callback();

View File

@ -0,0 +1,200 @@
#include <byte_buffer.h>
#include <cstdlib>
#include <algorithm>
#include <cstring>
#include <osapi.h>
#include <libb64/cdecode.h>
#include <libb64/cencode.h>
namespace utils
{
byte_buffer::byte_buffer(uint_fast16_t length)
: _length(length)
{
// +1 to allow this to be safely printed as a char
_buffer = reinterpret_cast<unsigned char *>(std::calloc(length + 1, sizeof(unsigned char)));
}
byte_buffer::~byte_buffer()
{
clear_buffer();
std::free(_buffer);
}
const bool byte_buffer::is_valid()
{
return _buffer != nullptr;
}
unsigned char *byte_buffer::get_ptr()
{
if (is_valid())
{
return _buffer;
}
else
{
return nullptr;
}
}
/////////////////////////////////////////////////////////////////////////////////////////
// Get the length of the buffer.
//
// Returns: The length of the buffer, or 0 if the buffer is invalid.
/////////////////////////////////////////////////////////////////////////////////////////
const uint_fast16_t byte_buffer::get_length()
{
if (is_valid())
{
return _length;
}
else
{
return 0;
}
}
/////////////////////////////////////////////////////////////////////////////////////////
// Get a pointer to a location in the buffer, length is updated to show the length
// available in that buffer
//
// index: The location to get a pointer to.
// length: The length of the data you expect to access from this pointer.
//
// Returns: A ptr to the buffer, or nullptr if reading that much data would cause
// an overflow
/////////////////////////////////////////////////////////////////////////////////////////
unsigned char *byte_buffer::get_ptr(const uint_fast16_t index, uint_fast16_t &length)
{
unsigned char *ret = nullptr;
if (is_valid() && _length > index)
{
length = _length - index;
ret = &_buffer[index];
}
else
{
length = 0;
}
return ret;
}
// Copy data into this buffer. The index value is updated to the index of the next point in the buffer
const bool byte_buffer::copy_from(const void *const sourceBuffer, uint_fast16_t &index, const uint_fast16_t &length)
{
auto ret = false;
if (is_valid() &&
sourceBuffer != nullptr &&
(index + length) <= _length)
{
std::memcpy(_buffer + index, sourceBuffer, length);
index += length;
ret = true;
}
return ret;
}
// Copy data out of this buffer. The index value is updated to the index of the next point in the buffer
const bool byte_buffer::copy_to(void *destinationBuffer, uint_fast16_t &index, const uint_fast16_t &length)
{
auto ret = false;
if (is_valid() &&
destinationBuffer != nullptr &&
(index + length) < _length)
{
std::memcpy(destinationBuffer, _buffer + index, length);
index += length;
ret = true;
}
return ret;
}
const bool byte_buffer::clone(byte_buffer &bufferToClone)
{
auto ret = false;
if (is_valid() &&
bufferToClone.is_valid() &&
bufferToClone.get_length() <= _length)
{
std::memcpy(bufferToClone.get_ptr(), 0, bufferToClone.get_length());
ret = true;
}
return ret;
}
const void byte_buffer::clear_buffer()
{
if (is_valid())
{
std::fill(_buffer, _buffer + _length, 0);
}
}
const void byte_buffer::fill_random()
{
if (is_valid())
{
os_get_random(_buffer, _length);
}
}
// Return the size of the data stored
const uint_fast16_t byte_buffer::load_base64(std::string base64Data)
{
auto read = 0u;
if (is_valid() && _length >= base64_decode_expected_len(base64Data.length()))
{
base64_decodestate state;
base64_init_decodestate(&state);
read = base64_decode_block(base64Data.c_str(), base64Data.length(), reinterpret_cast<char *>(_buffer), &state);
}
return read;
}
//// Get a base64 encoded string of this buffer.
const std::string byte_buffer::get_base64()
{
if (!is_valid())
{
return "";
}
auto b64String = std::string("");
auto size = base64_encode_expected_len_nonewlines(_length) + 1;
auto buffer = reinterpret_cast<char *>(std::calloc(size, 1));
if (buffer != nullptr)
{
base64_encodestate state;
base64_init_encodestate_nonewlines(&state);
auto encoded = base64_encode_block(reinterpret_cast<const char *>(_buffer), _length, buffer, &state);
encoded = base64_encode_blockend(buffer + encoded, &state);
b64String = std::string(buffer);
std::free(buffer);
}
return b64String;
}
const std::string byte_buffer::to_string()
{
if (!is_valid())
{
return "";
}
// Make sure that extra byte really is null!
_buffer[_length] = '\0';
return std::string(reinterpret_cast<char *>(_buffer));
}
} // namespace Control_System

View File

@ -0,0 +1,38 @@
#pragma once
#include <stdint.h>
#include <string>
namespace utils
{
class byte_buffer
{
public:
byte_buffer(uint_fast16_t length);
~byte_buffer();
const bool is_valid();
unsigned char *get_ptr();
const uint_fast16_t get_length();
unsigned char *get_ptr(const uint_fast16_t index, uint_fast16_t &length);
const bool copy_to(void *destinationBuffer, uint_fast16_t &index, const uint_fast16_t &length);
const bool copy_from(const void *const sourceBuffer, uint_fast16_t &index, const uint_fast16_t &length);
const bool clone(byte_buffer &bufferToClone);
const void clear_buffer();
const void fill_random();
const uint_fast16_t load_base64(std::string base64Data);
const std::string get_base64();
const std::string to_string();
private:
unsigned char *_buffer;
const uint_fast16_t _length;
};
} // namespace Control_System

View File

@ -15,3 +15,4 @@ framework = arduino
lib_deps = lib_deps =
olikraus/U8g2@^2.28.8 olikraus/U8g2@^2.28.8
finitespace/BME280@^3.0.0 finitespace/BME280@^3.0.0
rweather/Crypto@^0.2.0

View File

@ -4,20 +4,39 @@
#include "setup_server.h" #include "setup_server.h"
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <string> #include <string>
#include "settings.h"
#include <ChaChaPoly.h>
#include <BLAKE2s.h>
Environment::Settings settings;
utils::byte_buffer passcode(DERIVATION_HASH_SIZE);
void setup() void setup()
{ {
settings.apiID = 11085093266951290551U;
settings.endpoint = std::string("http://192.168.64.244:8080/data/authed/");
settings.passcode = std::string("password");
Serial.begin(9600); Serial.begin(9600);
Serial.println("\nSTART"); Serial.println("\nSTART");
Serial.print("Last Shutdown: "); Serial.print("Last Shutdown: ");
Serial.println(ESP.getResetReason()); Serial.println(ESP.getResetReason());
Serial.print("Derive Key: ");
DeriveKey();
// put your setup code here, to run once: // put your setup code here, to run once:
screen.begin(); screen.begin();
Wire.begin(); Wire.begin();
bme.begin(); bme.begin();
environmentUpdate.attach_scheduled(1, UpdateEnvironment); environmentUpdate.attach_scheduled(1, UpdateEnvironment);
environmentPost.attach_scheduled(60, PostEnvironment); environmentPost.attach_scheduled(60, PostEnvironment);
screen.firstPage();
do
{
screen.setFont(FONT);
screen.drawStr(2, LINE_1, "Connecting...");
} while (screen.nextPage());
InitializeWifi(); InitializeWifi();
} }
@ -25,16 +44,18 @@ void loop()
{ {
// put your main code here, to run repeatedly: // put your main code here, to run repeatedly:
screen.firstPage(); screen.firstPage();
do{ do
{
screen.setFont(FONT); screen.setFont(FONT);
screen.drawStr(2, LINE_1, "Hello World!"); screen.drawStr(2, LINE_1, "Hello World!");
screen.drawStr(2, LINE_2, "Hi Butlersaurus!"); screen.drawStr(2, LINE_2, "Hi Butlersaurus!");
}while(screen.nextPage()); } while (screen.nextPage());
delay(1000); delay(1000);
screen.firstPage(); screen.firstPage();
do{ do
{
screen.setFont(FONT); screen.setFont(FONT);
screen.drawStr(2, LINE_1, TEMP); screen.drawStr(2, LINE_1, TEMP);
screen.setCursor(screen.getStrWidth(TEMP), LINE_1); screen.setCursor(screen.getStrWidth(TEMP), LINE_1);
@ -42,35 +63,68 @@ void loop()
screen.drawStr(2, LINE_2, HUMID); screen.drawStr(2, LINE_2, HUMID);
screen.setCursor(screen.getStrWidth(HUMID), LINE_2); screen.setCursor(screen.getStrWidth(HUMID), LINE_2);
screen.print(humidity); screen.print(humidity);
}while(screen.nextPage()); } while (screen.nextPage());
delay(1000); delay(1000);
} }
void UpdateEnvironment() void UpdateEnvironment()
{ {
bme.read(pressure, temp, humidity,tempUnit, presUnit); bme.read(pressure, temp, humidity, tempUnit, presUnit);
} }
void PostEnvironment() void PostEnvironment()
{ {
utils::debug_print("Post"); utils::debug_print("Post");
requests.begin(client, "http://192.168.64.239/api/environment"); //requests.begin(client, "http://192.168.64.239/api/environment");
auto endpoint = std::string(settings.endpoint);
endpoint.append(std::to_string(settings.apiID));
requests.begin(client, endpoint.c_str());
requests.addHeader("Content-Type", "application/json"); requests.addHeader("Content-Type", "application/json");
char dataBuffer[128]; char dataBuffer[128] = {0};
char formatString[] = "{\"h\":\"%f\",\"t\":\"%f\"}"; char formatString[] = "{\"h\":%f,\"t\":%f}";
snprintf(&dataBuffer[0], 128, &formatString[0], humidity, temp); snprintf(&dataBuffer[0], 128, &formatString[0], humidity, temp);
auto code = requests.POST(dataBuffer); auto data = Encrypt(std::string(dataBuffer));
if (code > 0){ auto code = requests.PUT(data.get_ptr(), data.get_length());
if (code > 0)
{
utils::debug_print(code); utils::debug_print(code);
}else{ }
else
{
utils::debug_print(requests.errorToString(code)); utils::debug_print(requests.errorToString(code));
} }
requests.end(); requests.end();
utils::debug_print("Posted"); utils::debug_print("Posted");
} }
utils::byte_buffer Encrypt(std::string data)
{
// struct used by go, with basically no docs :|
// iv (12) | cipher (x) | tag(16)
const uint_fast16_t ivSize = 12;
auto size = ivSize + data.length() + 16;
auto dataOut = utils::byte_buffer(size);
// for the iv
dataOut.fill_random();
// first 12 bytes
auto cipher = ChaChaPoly();
cipher.setKey(passcode.get_ptr(), passcode.get_length());
cipher.setIV(dataOut.get_ptr(), ivSize);
uint_fast16_t buffLen = 0;
auto encryptBuffer = dataOut.get_ptr(ivSize, buffLen);
cipher.encrypt(encryptBuffer, (const uint8_t *)data.c_str(), data.length());
auto tagBuffer = dataOut.get_ptr(ivSize + data.length(), buffLen);
cipher.computeTag(tagBuffer, buffLen);
return dataOut;
}
void InitializeWifi() void InitializeWifi()
{ {
delay(1);
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(); WiFi.begin();
auto ret = WiFi.waitForConnectResult(); auto ret = WiFi.waitForConnectResult();
@ -83,6 +137,16 @@ void InitializeWifi()
std::string password = std::string(); std::string password = std::string();
setup.get_connection(ssid, password); setup.get_connection(ssid, password);
} }
WiFi.setAutoConnect(true);
utils::debug_print("Wifi Connected: " + WiFi.SSID()); utils::debug_print("Wifi Connected: " + WiFi.SSID());
} }
void DeriveKey()
{
BLAKE2s hash = BLAKE2s();
hash.reset();
hash.update(DERIVATION_SALT, strlen(DERIVATION_SALT));
hash.update(settings.passcode.c_str(), settings.passcode.length());
hash.finalize(passcode.get_ptr(), passcode.get_length());
utils::debug_print(passcode.get_base64());
}

View File

@ -1,15 +1,15 @@
#include <setup_server.h> #include <setup_server.h>
#include <string>
#include <html/first_connection_page.h> #include <html/first_connection_page.h>
#ifdef _DEBUG #ifdef _DEBUG
#define _DEBUG_PRINT(x) utils::debug_print(x) #define _DEBUG_PRINT(x) utils::debug_print(x)
#else #else
#define _DEBUG_PRINT(x) #define _DEBUG_PRINT(x)
#endif #endif
setup_server::setup_server() setup_server::setup_server()
: server(new ESP8266WebServer(80)) : server(new ESP8266WebServer(80))
{ {
server->on("/", [this] { this->root_callback(); }); server->on("/", [this] { this->root_callback(); });
server->on("/submit", HTTP_POST, [this] { this->ap_submission_callback(); }); server->on("/submit", HTTP_POST, [this] { this->ap_submission_callback(); });
@ -53,7 +53,7 @@ setup_server::SetupErrorCodes setup_server::get_connection(const std::string &ss
return ret; return ret;
} }
uint8_t setup_server::connect_to_ap(const std::string ssid, const std::string password) uint8_t setup_server::connect_to_ap(const std::string &ssid, const std::string &password)
{ {
_DEBUG_PRINT("Connecting " + ssid + ":" + password); _DEBUG_PRINT("Connecting " + ssid + ":" + password);
WiFi.begin(ssid.c_str(), password.c_str()); WiFi.begin(ssid.c_str(), password.c_str());
@ -65,10 +65,11 @@ void setup_server::root_callback()
{ {
_DEBUG_PRINT("Request \"/\" from " + server->client().remoteIP().toString()); _DEBUG_PRINT("Request \"/\" from " + server->client().remoteIP().toString());
auto pageSize = strlen_P(first_connection_page); auto pageSize = strlen_P(first_connection_page);
auto outBuffer = new char(pageSize); auto outBuffer = new char[pageSize + 1];
strncpy_P(outBuffer, first_connection_page, pageSize); strncpy_P(outBuffer, first_connection_page, pageSize);
outBuffer[pageSize] = '\0';
server->send(200, "text/html", outBuffer); server->send(200, "text/html", outBuffer);
delete outBuffer; delete[] outBuffer;
} }
void setup_server::ap_submission_callback() void setup_server::ap_submission_callback()

View File

@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using MongoDB.Bson; using MongoDB.Bson;
using MongoDB.Driver; using MongoDB.Driver;
using System.Security.Claims;
namespace ManagementPage.Pages namespace ManagementPage.Pages
{ {
@ -44,16 +45,34 @@ namespace ManagementPage.Pages
return NotFound(); return NotFound();
} }
var filter = new BsonDocument("ApiID", deviceId); // get the logged in user
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var filter = new BsonDocument("OpenId", userId);
var claims = await _dbClient.AccountsCollection.Find(filter).SingleOrDefaultAsync();
// No user?
if (claims == null)
{
return Forbid();
}
// Get the device
filter = new BsonDocument("ApiID", deviceId);
device = await _dbClient.DeviceCollection.Find(filter).FirstOrDefaultAsync(); device = await _dbClient.DeviceCollection.Find(filter).FirstOrDefaultAsync();
FindOptions<EnvironmentData> options = new FindOptions<EnvironmentData> // No device?
if (device == null)
{ {
Limit = 100, return NotFound();
NoCursorTimeout = false }
};
using var cursor = await _dbClient.Collection.FindAsync(filter, options); // Device owned by someone else?
if (!claims.Devices.Contains(device._id))
{
return Forbid();
}
using var cursor = await _dbClient.Collection.Find(filter).SortByDescending(x => x.Time).Limit(100).ToCursorAsync();
data.AddRange(await cursor.ToListAsync()); data.AddRange(await cursor.ToListAsync());
foreach (var item in data) foreach (var item in data)
{ {
@ -61,6 +80,9 @@ namespace ManagementPage.Pages
temperature.Add(new DataSet() { x = item.Time, y = item.Temperature }); temperature.Add(new DataSet() { x = item.Time, y = item.Temperature });
} }
humidity.Reverse();
temperature.Reverse();
return Page(); return Page();
} }
} }

View File

@ -51,7 +51,7 @@ namespace ManagementPage.Pages
foreach (var item in data) foreach (var item in data)
{ {
var filter = new BsonDocument("ApiID", item.ApiID); var filter = new BsonDocument("ApiID", item.ApiID);
var envData = await _dbClient.Collection.Find(filter).FirstOrDefaultAsync(); var envData = await _dbClient.Collection.Find(filter).SortByDescending(x => x.Time).FirstOrDefaultAsync();
envData ??= new EnvironmentData(item.ApiID, 0, 0, DateTime.UtcNow); envData ??= new EnvironmentData(item.ApiID, 0, 0, DateTime.UtcNow);
currentEnvironment.Add(item.ApiID, envData); currentEnvironment.Add(item.ApiID, envData);
} }
@ -65,9 +65,15 @@ namespace ManagementPage.Pages
} }
var id = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(),4); var id = BitConverter.ToInt64(Guid.NewGuid().ToByteArray(),4);
var device = new DeviceData(NewDevice.Name, NewDevice.Passcode, id); var device = new DeviceData(NewDevice.Name, NewDevice.Passcode, id);
device._id = ObjectId.GenerateNewId();
await _dbClient.DeviceCollection.InsertOneAsync(device); await _dbClient.DeviceCollection.InsertOneAsync(device);
return Page(); var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var filter = new BsonDocument("OpenId", userId);
var update = Builders<UserData>.Update.AddToSet("Devices", device._id);
await _dbClient.AccountsCollection.UpdateOneAsync(filter, update);
return new RedirectToPageResult("/UserHome");
} }
} }
} }

View File

@ -12,7 +12,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"applicationUrl": "https://localhost:8800;http://+:80", "applicationUrl": "https://environment.51m0n.com;http://+:80",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -8,5 +8,7 @@
}, },
"oidc": { "oidc": {
"region": "openid-connect", "region": "openid-connect",
"clientid": "51m0n-temperature",
"clientsecret": "577b32ae-d36e-42ec-b6a2-92025dc16619"
} }
} }