diff --git a/.gitignore b/.gitignore
index 5fd29cd..b62bf52 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
node_modules/
-result/
\ No newline at end of file
+result/
+lcov.info
\ No newline at end of file
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..437e518
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "testFrontend/ArcaneLibs"]
+ path = testFrontend/ArcaneLibs
+ url = https://github.com/TheArcaneBrony/ArcaneLibs.git
diff --git a/.idea/prettier.xml b/.idea/prettier.xml
new file mode 100644
index 0000000..0c83ac4
--- /dev/null
+++ b/.idea/prettier.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="PrettierConfiguration">
+ <option name="myConfigurationMode" value="AUTOMATIC" />
+ <option name="myRunOnSave" value="true" />
+ </component>
+</project>
\ No newline at end of file
diff --git a/hashes.json b/hashes.json
index bd1a93e..707146c 100644
--- a/hashes.json
+++ b/hashes.json
@@ -1,3 +1,3 @@
{
- "npmDepsHash": "sha256-d3l0xMvkcBSGrC15dE8rE4+brVzi6YIfQBTksqCYD10="
+ "npmDepsHash": "sha256-Rge1bomfj8Xgn8gPmcKNHyZBtVQY2lY6LFNRnOoHWcM="
}
diff --git a/package-lock.json b/package-lock.json
index 591682a..c2d845f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,8 @@
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
+ "bcrypt": "^6.0.0",
+ "dotenv": "^16.5.0",
"eslint": "^9.27.0",
"globals": "^16.2.0",
"husky": "^9.1.7",
@@ -421,6 +423,21 @@
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
+ "node_modules/bcrypt": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^8.3.0",
+ "node-gyp-build": "^4.8.4"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -646,6 +663,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/dotenv": {
+ "version": "16.5.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
+ "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -1718,6 +1748,28 @@
"node": ">= 0.6"
}
},
+ "node_modules/node-addon-api": {
+ "version": "8.3.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz",
+ "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18 || ^20 || >= 21"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
diff --git a/package.json b/package.json
index 1d4c039..437858d 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,10 @@
"main": "src/api/start.js",
"type": "module",
"scripts": {
- "prepare": "husky install"
+ "prepare": "husky install",
+ "test": "node --test",
+ "test:watch": "node --test --watch",
+ "coverage": "node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info"
},
"repository": {
"type": "git",
@@ -16,11 +19,14 @@
"imports": {
"#api/*": "./src/api/*",
"#db/*": "./src/db/*",
- "#util/*": "./src/util/*"
+ "#util/*": "./src/util/*",
+ "#tests/*": "./src/tests/*"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.27.0",
+ "bcrypt": "^6.0.0",
+ "dotenv": "^16.5.0",
"eslint": "^9.27.0",
"globals": "^16.2.0",
"husky": "^9.1.7",
diff --git a/src/db/dbAccess/index.js b/src/db/dbAccess/index.js
new file mode 100644
index 0000000..ee1c337
--- /dev/null
+++ b/src/db/dbAccess/index.js
@@ -0,0 +1 @@
+export * from './user.js';
diff --git a/src/db/dbAccess/user.js b/src/db/dbAccess/user.js
new file mode 100644
index 0000000..413b1cf
--- /dev/null
+++ b/src/db/dbAccess/user.js
@@ -0,0 +1,41 @@
+import { hash, compare } from 'bcrypt';
+import { DbUser } from '#db/schemas/index.js';
+
+export async function registerUser(username, password, email, type = 'user') {
+ if (!username || !password || !email) {
+ throw new Error(
+ 'Username, password, and email are required to register a user.'
+ );
+ }
+
+ const passwordHash = await hash(password, 10);
+ if (!passwordHash) {
+ throw new Error('Failed to hash password.');
+ }
+
+ return DbUser.create({
+ username,
+ passwordHash,
+ email,
+ type
+ });
+}
+
+export async function deleteUser(id, password) {
+ const user = await DbUser.findById(id);
+ DbUser.exists({ _id: id }).then(exists => {
+ if (!exists) {
+ throw new Error('User does not exist.');
+ }
+ });
+ if (!user) {
+ throw new Error('User not found.');
+ }
+
+ const isPasswordValid = await compare(password, user.passwordHash);
+ if (!isPasswordValid) {
+ throw new Error('Invalid password.');
+ }
+
+ await DbUser.findByIdAndDelete(id);
+}
diff --git a/src/db/dbAccess/user.test.js b/src/db/dbAccess/user.test.js
new file mode 100644
index 0000000..e8e1af5
--- /dev/null
+++ b/src/db/dbAccess/user.test.js
@@ -0,0 +1,44 @@
+import * as dotenv from 'dotenv';
+import { it } from 'node:test';
+import { deleteUser, registerUser } from '#db/index.js';
+import * as assert from 'node:assert';
+import { initDb } from '#db/db.js';
+import { disconnect } from 'mongoose';
+
+dotenv.config();
+
+await initDb();
+
+async function createTestUser() {
+ const username = (Math.random() * 1000000).toString();
+ const password = (Math.random() * 1000000).toString();
+ const email = (Math.random() * 1000000).toString() + '@example.com';
+ return {
+ username,
+ password,
+ email,
+ user: await registerUser(username, password, email)
+ };
+}
+
+await it('Can create user', async () => {
+ await assert.doesNotReject(createTestUser());
+});
+
+await it('Can delete user', async () => {
+ const { username, password, email, user } = await createTestUser();
+
+ const deletePromise = deleteUser(user._id, password);
+ await assert.doesNotReject(deletePromise);
+});
+
+await it('Cant delete nonexistant user', async () => {
+ await assert.rejects(deleteUser('abc', ''));
+});
+
+await it('Cant delete user with invalid password', async () => {
+ const user = await createTestUser();
+ await assert.rejects(deleteUser(user.user._id, 'wrongpassword'));
+});
+
+await disconnect();
diff --git a/src/db/index.js b/src/db/index.js
index cd306b7..0680880 100644
--- a/src/db/index.js
+++ b/src/db/index.js
@@ -1,2 +1,3 @@
export * from './db.js';
export * from './schemas/index.js';
+export * from './dbAccess/index.js';
diff --git a/src/db/schemas/user.js b/src/db/schemas/user.js
index 1a7e048..9d08680 100644
--- a/src/db/schemas/user.js
+++ b/src/db/schemas/user.js
@@ -1,4 +1,5 @@
import { model, Schema } from 'mongoose';
+import { hash, compare } from 'bcrypt';
/**
* User schema for MongoDB.
diff --git a/src/util/error.js b/src/util/error.js
new file mode 100644
index 0000000..96f1deb
--- /dev/null
+++ b/src/util/error.js
@@ -0,0 +1,36 @@
+export class SafeNSoundError extends Error {
+ constructor(options) {
+ super();
+ if (typeof options === 'string') {
+ this.errCode = options;
+ super.message = this.getDefaultMessage();
+ } else if (typeof options === 'object') {
+ this.errCode = options.errCode || 'UNKNOWN_ERROR';
+ super.message = options.message || this.getDefaultMessage();
+ } else {
+ this.errCode = 'UNKNOWN_ERROR';
+ this.message =
+ 'An unknown error occurred (invalid SafeNSoundError constructor options)';
+ }
+ }
+
+ getDefaultMessage() {
+ switch (this.type) {
+ case 'USER_NOT_FOUND':
+ return 'User not found';
+ case 'INVALID_CREDENTIALS':
+ return 'Invalid credentials';
+ case 'EMAIL_ALREADY_EXISTS':
+ return 'Email already exists';
+ case 'PASSWORD_TOO_WEAK':
+ return 'Password is too weak';
+ case 'TOKEN_EXPIRED':
+ return 'Token has expired';
+ case 'UNAUTHORIZED':
+ return 'Unauthorized access';
+ case 'UNKNOWN_ERROR':
+ default:
+ return 'An unknown error occurred';
+ }
+ }
+}
diff --git a/src/util/jwtUtils.js b/src/util/jwtUtils.js
index 115c9c5..9031631 100644
--- a/src/util/jwtUtils.js
+++ b/src/util/jwtUtils.js
@@ -1,6 +1,7 @@
import {existsSync} from 'fs';
import {readFile, writeFile} from "node:fs/promises";
import {generateKeyPairSync, createHash, createPublicKey, createPrivateKey} from 'node:crypto';
+import jwt from "jsonwebtoken";
let privateKey, publicKey, fingerprint;
@@ -10,6 +11,8 @@ export async function initJwt() {
throw new Error('JWT secret path is not defined in environment variables, or the directory does not exist.');
}
+ console.log(`[JWT] Initializing JWT with secret path: ${secretPath}`);
+
const privateKeyPath = `${secretPath}/jwt.key`;
const publicKeyPath = `${secretPath}/jwt.key.pub`;
@@ -55,9 +58,40 @@ const jwtOptions = {
}
export async function generateJwtToken(user) {
+ if (!privateKey) {
+ throw new Error('JWT private key is not initialized. Please call initJwt() first.');
+ }
+
+ const payload = {
+ sub: user._id.toString(),
+ username: user.username,
+ type: user.type,
+ iat: Math.floor(Date.now() / 1000)
+ };
+ return new Promise((resolve, reject) => {
+ jwt.sign(payload, privateKey, jwtOptions, (err, token) => {
+ if (err) {
+ console.error('[JWT] Error generating token:', err);
+ return reject(err);
+ }
+ resolve(token);
+ });
+ });
}
export async function validateJwtToken(token) {
+ if (!publicKey) {
+ throw new Error('JWT public key is not initialized. Please call initJwt() first.');
+ }
+ return new Promise((resolve, reject) => {
+ jwt.verify(token, publicKey, jwtOptions, (err, decoded) => {
+ if (err) {
+ console.error('[JWT] Token validation failed:', err);
+ return reject(err);
+ }
+ resolve(decoded);
+ });
+ });
}
\ No newline at end of file
diff --git a/src/util/jwtUtils.test.js b/src/util/jwtUtils.test.js
new file mode 100644
index 0000000..94a7d99
--- /dev/null
+++ b/src/util/jwtUtils.test.js
@@ -0,0 +1,35 @@
+import { it } from 'node:test';
+import { initJwt, generateJwtToken, validateJwtToken } from './jwtUtils.js';
+import * as dotenv from 'dotenv';
+import { tmpdir } from 'node:os';
+import { mkdtemp } from 'node:fs/promises';
+import * as assert from 'node:assert';
+dotenv.config();
+
+const user = {
+ _id: 'meow',
+ username: 'testuser'
+};
+
+await it('Should be able to generate new secrets', async () => {
+ process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir());
+ await initJwt();
+});
+
+await it('Should be able to load the JWT utils', async () => {
+ process.env.JWT_SECRET_PATH = await mkdtemp(tmpdir());
+ await initJwt();
+ await initJwt();
+});
+
+it('Should be able to create a JWT token', async () => {
+ await initJwt();
+ await generateJwtToken(user);
+});
+
+it('Should be able to validate a JWT token', async () => {
+ await initJwt();
+ const token = await generateJwtToken(user);
+ const result = await validateJwtToken(token);
+ console.log(result);
+});
diff --git a/src/util/secretUtils.js b/src/util/secretUtils.js
index 92e1b1c..7394395 100644
--- a/src/util/secretUtils.js
+++ b/src/util/secretUtils.js
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
export async function readSecret(name, path) {
console.log(`[SECRET] Reading secret "${name}" from path: ${path}`);
if (!path) {
- throw new Error('Path to secret file is required');
+ throw new Error(`Path to secret file is required: ${name}`);
}
const content = await fs.readFile(path, 'utf8');
return content.trim();
diff --git a/src/util/secretUtils.test.js b/src/util/secretUtils.test.js
new file mode 100644
index 0000000..d95d56f
--- /dev/null
+++ b/src/util/secretUtils.test.js
@@ -0,0 +1,23 @@
+import { it } from 'node:test';
+import * as dotenv from 'dotenv';
+import { tmpdir } from 'node:os';
+import * as assert from 'node:assert';
+import { readSecret } from '#util/secretUtils.js';
+import { writeFile, mkdtemp } from 'node:fs/promises';
+
+dotenv.config();
+
+await it('Should fail on nonexistant secret', async () => {
+ const secretPath = (await mkdtemp(tmpdir())) + '/secret';
+ await assert.rejects(readSecret('Nonexistant secret', secretPath));
+});
+
+await it('Should read secret from file', async () => {
+ const secretPath = (await mkdtemp(tmpdir())) + '/secret';
+ await writeFile(secretPath, 'testsecret');
+ await assert.doesNotReject(readSecret('Test secret', secretPath));
+ await assert.equal(
+ await readSecret('Test secret', secretPath),
+ 'testsecret'
+ );
+});
diff --git a/testFrontend/ArcaneLibs b/testFrontend/ArcaneLibs
new file mode 160000
+Subproject 58a531f72aaae7d44420fcaa7feb5f494c06997
diff --git a/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj b/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj
index 7bccb06..06f60b4 100644
--- a/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj
+++ b/testFrontend/SafeNSound.Frontend/SafeNSound.Frontend.csproj
@@ -17,6 +17,7 @@
</ItemGroup>
<ItemGroup>
+ <ProjectReference Include="..\ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj" />
<ProjectReference Include="..\SafeNSound.Sdk\SafeNSound.Sdk.csproj" />
</ItemGroup>
diff --git a/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj b/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj
index 7338078..1bf7de5 100644
--- a/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj
+++ b/testFrontend/SafeNSound.Sdk/SafeNSound.Sdk.csproj
@@ -8,9 +8,11 @@
</PropertyGroup>
<ItemGroup>
- <Folder Include="Clients\AdminClient\" />
- <Folder Include="Clients\MonitorClient\" />
- <Folder Include="Clients\UserClient\" />
+ <Folder Include="Clients\" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" />
</ItemGroup>
</Project>
diff --git a/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs b/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
index 23c1445..7d88ec8 100644
--- a/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
+++ b/testFrontend/SafeNSound.Sdk/SafeNSoundAuthentication.cs
@@ -1,6 +1,6 @@
namespace SafeNSound.Sdk;
-public class SafeNSoundAuthentication(SafeNSoundConfiguration)
+public class SafeNSoundAuthentication(SafeNSoundConfiguration config)
{
public async Task<SafeNSoundAuthResult> Login(string username, string password)
{
@@ -11,4 +11,9 @@ public class SafeNSoundAuthentication(SafeNSoundConfiguration)
{
}
+}
+
+public class SafeNSoundAuthResult
+{
+
}
\ No newline at end of file
diff --git a/testFrontend/SafeNSound.sln b/testFrontend/SafeNSound.sln
index 6fc2336..c5bee09 100644
--- a/testFrontend/SafeNSound.sln
+++ b/testFrontend/SafeNSound.sln
@@ -4,19 +4,166 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeNSound.Frontend", "Safe
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SafeNSound.Sdk", "SafeNSound.Sdk\SafeNSound.Sdk.csproj", "{05CB21DB-72C4-495B-BED4-3B85CC51AFDA}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ArcaneLibs", "ArcaneLibs", "{24C94C05-725E-242A-3195-1FB70FB907A6}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs", "ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj", "{16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{F7CB0AF0-CD59-42C7-874D-79EA82753FAC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{09822D71-D77A-4846-A7CA-BE7997112D2F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{62ABA511-FC64-4630-856C-BD70C4FFDB09}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Tests", "ArcaneLibs\ArcaneLibs.Tests\ArcaneLibs.Tests.csproj", "{BBE6F71B-CBD0-470C-A484-D0656A9B11BF}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{198BBB21-4AA1-4753-BC33-39AFFAA88999}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{24FC30D3-E68C-471D-99DA-63C469C3262C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x64.Build.0 = Debug|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Debug|x86.Build.0 = Debug|Any CPU
{F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x64.ActiveCfg = Release|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x64.Build.0 = Release|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x86.ActiveCfg = Release|Any CPU
+ {F5B8AE63-18E8-4447-8A5D-BF0872255F34}.Release|x86.Build.0 = Release|Any CPU
{05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x64.Build.0 = Debug|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Debug|x86.Build.0 = Debug|Any CPU
{05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x64.ActiveCfg = Release|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x64.Build.0 = Release|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x86.ActiveCfg = Release|Any CPU
+ {05CB21DB-72C4-495B-BED4-3B85CC51AFDA}.Release|x86.Build.0 = Release|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x64.Build.0 = Debug|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Debug|x86.Build.0 = Debug|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x64.ActiveCfg = Release|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x64.Build.0 = Release|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x86.ActiveCfg = Release|Any CPU
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9}.Release|x86.Build.0 = Release|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x64.Build.0 = Debug|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Debug|x86.Build.0 = Debug|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x64.ActiveCfg = Release|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x64.Build.0 = Release|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x86.ActiveCfg = Release|Any CPU
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC}.Release|x86.Build.0 = Release|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x64.Build.0 = Debug|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Debug|x86.Build.0 = Debug|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x64.ActiveCfg = Release|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x64.Build.0 = Release|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x86.ActiveCfg = Release|Any CPU
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2}.Release|x86.Build.0 = Release|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x64.Build.0 = Debug|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Debug|x86.Build.0 = Debug|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x64.ActiveCfg = Release|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x64.Build.0 = Release|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x86.ActiveCfg = Release|Any CPU
+ {09822D71-D77A-4846-A7CA-BE7997112D2F}.Release|x86.Build.0 = Release|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x64.Build.0 = Debug|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Debug|x86.Build.0 = Debug|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|Any CPU.Build.0 = Release|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x64.ActiveCfg = Release|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x64.Build.0 = Release|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x86.ActiveCfg = Release|Any CPU
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09}.Release|x86.Build.0 = Release|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x64.Build.0 = Debug|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Debug|x86.Build.0 = Debug|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x64.ActiveCfg = Release|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x64.Build.0 = Release|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x86.ActiveCfg = Release|Any CPU
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF}.Release|x86.Build.0 = Release|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x64.Build.0 = Debug|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Debug|x86.Build.0 = Debug|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|Any CPU.Build.0 = Release|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x64.ActiveCfg = Release|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x64.Build.0 = Release|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x86.ActiveCfg = Release|Any CPU
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999}.Release|x86.Build.0 = Release|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x64.Build.0 = Debug|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Debug|x86.Build.0 = Debug|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x64.ActiveCfg = Release|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x64.Build.0 = Release|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x86.ActiveCfg = Release|Any CPU
+ {24FC30D3-E68C-471D-99DA-63C469C3262C}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {16BCBEB8-A3CA-4212-B0EF-569CAC3E14A9} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {F7CB0AF0-CD59-42C7-874D-79EA82753FAC} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {22FE30E8-DCCA-42F7-9D81-5E0D3111C0A2} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {09822D71-D77A-4846-A7CA-BE7997112D2F} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {62ABA511-FC64-4630-856C-BD70C4FFDB09} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {BBE6F71B-CBD0-470C-A484-D0656A9B11BF} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {198BBB21-4AA1-4753-BC33-39AFFAA88999} = {24C94C05-725E-242A-3195-1FB70FB907A6}
+ {24FC30D3-E68C-471D-99DA-63C469C3262C} = {24C94C05-725E-242A-3195-1FB70FB907A6}
EndGlobalSection
EndGlobal
|