This commit is contained in:
Sina Blattmann 2023-03-16 08:39:57 +01:00
commit 79ec12ba5e
34 changed files with 1205 additions and 1035 deletions

View File

@ -1,438 +1,283 @@
using System.Net;
using System.Text;
using System.Web.Http;
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Model; using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.Model.Relations; using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Utils; using InnovEnergy.App.Backend.Relations;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using HttpContextAccessor = Microsoft.AspNetCore.Http.HttpContextAccessor; using static System.Net.HttpStatusCode;
using static System.String;
using Folder = InnovEnergy.App.Backend.DataTypes.Folder;
using Installation = InnovEnergy.App.Backend.DataTypes.Installation;
using Object = System.Object;
using User = InnovEnergy.App.Backend.DataTypes.User;
namespace InnovEnergy.App.Backend.Controllers; namespace InnovEnergy.App.Backend.Controllers;
[ApiController] [ApiController]
[Microsoft.AspNetCore.Mvc.Route("api/")] [Route("api/")]
public class Controller public class Controller
{ {
private static readonly HttpResponseMessage _Unauthorized = new HttpResponseMessage(Unauthorized);
private static readonly HttpResponseMessage _Ok = new HttpResponseMessage(OK);
private static readonly HttpResponseMessage _BadRequest = new HttpResponseMessage(BadRequest);
[Returns<String>] [Returns<String>]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Returns(HttpStatusCode.BadRequest)] [Returns(BadRequest)]
[Microsoft.AspNetCore.Mvc.HttpPost($"{nameof(Login)}")] [HttpPost($"{nameof(Login)}")]
public Object Login(Credentials credentials) public Object Login(Credentials credentials)
{ {
if (String.IsNullOrWhiteSpace(credentials.Username) || var session = credentials.Login();
String.IsNullOrWhiteSpace(credentials.Password))
return new HttpResponseException(HttpStatusCode.BadRequest);
using var db = Db.Connect(); return session is null
var user = db.GetUserByEmail(credentials.Username); ? _Unauthorized
: session;
if (user is null)
return new HttpResponseException(HttpStatusCode.BadRequest);
if (!VerifyPassword(credentials.Password, user))
return new HttpResponseException(HttpStatusCode.Unauthorized);
var ses = new Session(user);
db.NewSession(ses);
return new {ses.Token, user.Language};
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpPost($"{nameof(Logout)}")] [HttpPost($"{nameof(Logout)}")]
public Object Logout() public Object Logout()
{ {
var caller = GetCaller(); var session = GetSession();
if (caller is null) return session.Logout()
return new HttpResponseMessage(HttpStatusCode.Unauthorized); ? _Ok
: _Unauthorized;
using var db = Db.Connect();
return db.DeleteSession(caller.Id);
}
[Returns(HttpStatusCode.OK)]
[Returns(HttpStatusCode.Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetInstallationS3Key)}")]
public Object GetInstallationS3Key(Int64 installationId)
{
var caller = GetCaller();
if (caller is null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
var installation = db
.GetAllAccessibleInstallations(caller)
.FirstOrDefault(i => i.Id == installationId);
if(installation == null)
{
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
}
var key = db.GetInstallationS3Key(installationId);
return key ?? db.CreateAndSaveInstallationS3ApiKey(installation);
} }
[Returns<User>] // [Returns<User>]
[Returns(HttpStatusCode.Unauthorized)] // [Returns(HttpStatusCode.Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetUserById)}")] // [HttpGet($"{nameof(GetUserById)}")]
public Object GetUserById(Int64 id) // public Object GetUserById(Int64 id)
{ // {
var caller = GetCaller(); // var caller = GetCaller();
if (caller is null) // if (caller is null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); // return new HttpResponseMessage(HttpStatusCode.Unauthorized);
//
using var db = Db.Connect(); // var user = Db.GetUserById(id);
//
// if (user is null || !caller.HasAccessTo(user))
// return new HttpResponseMessage(HttpStatusCode.Unauthorized);
//
// return user;
// }
var user = db //
.GetDescendantUsers(caller) // [Returns<Installation>]
.FirstOrDefault(u => u.Id == id); // [Returns(HttpStatusCode.Unauthorized)]
// [HttpGet($"{nameof(GetInstallationById)}")]
return user as Object ?? new HttpResponseMessage(HttpStatusCode.Unauthorized); // public Object GetInstallationById(Int64 id)
} // {
// var caller = GetCaller();
// if (caller == null)
// return new HttpResponseMessage(HttpStatusCode.Unauthorized);
//
// var installation = Db.GetInstallationById(id);
//
// if (installation is null || !caller.HasAccessTo(installation))
// return new HttpResponseMessage(HttpStatusCode.Unauthorized);
//
// return installation;
// }
[Returns<Installation>] // [Returns<Folder>]
[Returns(HttpStatusCode.Unauthorized)] // [Returns(HttpStatusCode.Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetInstallationById)}")] // [HttpGet($"{nameof(GetFolderById)}")]
public Object GetInstallationById(Int64 id) // public Object GetFolderById(Int64 id)
{ // {
var caller = GetCaller(); // var caller = GetCaller();
if (caller == null) // if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); // return new HttpResponseMessage(HttpStatusCode.Unauthorized);
//
using var db = Db.Connect(); // var folder = Db.GetFolderById(id);
//
var installation = db // if (folder is null || !caller.HasAccessTo(folder))
.GetAllAccessibleInstallations(caller) // return new HttpResponseMessage(HttpStatusCode.Unauthorized);
.FirstOrDefault(i => i.Id == id); //
// return folder;
return installation as Object ?? new HttpResponseMessage(HttpStatusCode.NotFound); // }
}
[Returns<Folder>]
[Returns(HttpStatusCode.Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetFolderById)}")]
public Object GetFolderById(Int64 id)
{
var caller = GetCaller();
if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
var folder = db
.GetAllAccessibleFolders(caller)
.FirstOrDefault(f => f.Id == id);
return folder as Object ?? new HttpResponseMessage(HttpStatusCode.NotFound);
}
[Returns<Installation[]>] // assuming swagger knows about arrays but not lists (JSON) [Returns<Installation[]>] // assuming swagger knows about arrays but not lists (JSON)
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetAllInstallations)}/")] [HttpGet($"{nameof(GetAllInstallations)}/")]
public Object GetAllInstallations() public Object GetAllInstallations()
{ {
var caller = GetCaller(); var user = GetSession()?.User;
if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
return db return user is null
.GetAllAccessibleInstallations(caller) ? _Unauthorized
.ToList(); // important! : user.AccessibleInstallations();
} }
[Returns<Folder[]>] // assuming swagger knows about arrays but not lists (JSON) [Returns<Folder[]>] // assuming swagger knows about arrays but not lists (JSON)
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetAllFolders)}/")] [HttpGet($"{nameof(GetAllFolders)}/")]
public Object GetAllFolders() public Object GetAllFolders()
{ {
var caller = GetCaller(); var user = GetSession()?.User;
if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); return user is null
? _Unauthorized
using var db = Db.Connect(); : user.AccessibleFolders();
return db
.GetAllAccessibleFolders(caller)
.ToList(); // important!
} }
[Returns<TreeNode[]>] // assuming swagger knows about arrays but not lists (JSON) // [Returns<Folder[]>] // assuming swagger knows about arrays but not lists (JSON)
[Returns(HttpStatusCode.Unauthorized)] // [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetTree)}/")] // [HttpGet($"{nameof(GetUsersOfFolder)}/")]
public Object GetTree() // public Object GetUsersOfFolder(Int64 folderId)
{ // {
var caller = GetCaller(); // var caller = GetCaller();
if (caller == null) // if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); // return new HttpResponseMessage(Unauthorized);
//
using var db = Db.Connect(); // var folder = Db.GetFolderById(folderId);
//
var folders = db // if (folder is null || !caller.HasAccessTo(folder))
.GetDirectlyAccessibleFolders(caller) // ReSharper disable once AccessToDisposedClosure // return new HttpResponseMessage(Unauthorized);
.Select(f => PopulateChildren(db, f)); //
// return descendantUsers;
var installations = db.GetDirectlyAccessibleInstallations(caller); // }
return folders
.Concat<TreeNode>(installations)
.ToList(); // important!
}
[Returns<TreeNode[]>] // assuming swagger knows about arrays but not lists (JSON) [Returns<TreeNode[]>] // assuming swagger knows about arrays but not lists (JSON)
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpGet($"{nameof(GetAllFoldersAndInstallations)}/")] [HttpGet($"{nameof(GetAllFoldersAndInstallations)}/")]
public Object GetAllFoldersAndInstallations() public Object GetAllFoldersAndInstallations()
{ {
var caller = GetCaller(); var user = GetSession()?.User;
if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
var folders = db.GetAllAccessibleFolders(caller) as IEnumerable<TreeNode>;
var installations = db.GetAllAccessibleInstallations(caller);
return folders return user is null
.Concat(installations) ? _Unauthorized
.ToList(); // important! : user.AccessibleFoldersAndInstallations();
} }
private static Folder PopulateChildren(Db db, Folder folder, HashSet<Int64>? hs = null)
{
// TODO: remove cycle detector
hs ??= new HashSet<Int64>();
if (!hs.Add(folder.Id))
throw new Exception("Cycle detected: folder " + folder.Id);
var installations = db.GetChildInstallations(folder);
var folders = db
.GetChildFolders(folder)
.Select(c => PopulateChildren(db, c, hs));
folder.Children = folders.Concat<TreeNode>(installations).ToList();
return folder;
}
[Returns(HttpStatusCode.OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(OK)]
[Microsoft.AspNetCore.Mvc.HttpPost($"{nameof(CreateUser)}/")] [Returns(Unauthorized)]
[HttpPost($"{nameof(CreateUser)}/")]
public Object CreateUser(User newUser) public Object CreateUser(User newUser)
{ {
var caller = GetCaller(); var session = GetSession();
using var db = Db.Connect();
if (caller == null || !caller.HasWriteAccess)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
newUser.ParentId = caller.Id; return session.Create(newUser)
? newUser
return db.CreateUser(newUser); : _Unauthorized ;
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpPost($"{nameof(CreateInstallation)}/")] [HttpPost($"{nameof(CreateInstallation)}/")]
public Object CreateInstallation(Installation installation) public Object CreateInstallation(Installation installation)
{ {
var caller = GetCaller(); var session = GetSession();
using var db = Db.Connect();
if (caller == null || !caller.HasWriteAccess)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
var id = db.CreateInstallation(installation);
return db.AddToAccessibleInstallations(caller.Id, id);
return session.Create(installation)
? installation
: _Unauthorized;
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpPost($"{nameof(CreateFolder)}/")] [Returns(InternalServerError)]
[HttpPost($"{nameof(CreateFolder)}/")]
public Object CreateFolder(Folder folder) public Object CreateFolder(Folder folder)
{ {
var caller = GetCaller(); var session = GetSession();
using var db = Db.Connect();
if (caller == null || !caller.HasWriteAccess)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
var id = db.CreateFolder(folder);
return db.AddToAccessibleFolders(caller.Id, id);
return session.Create(folder)
? folder
: _Unauthorized;
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpPut($"{nameof(UpdateUser)}/")] [HttpPut($"{nameof(UpdateUser)}/")]
public Object UpdateUser(User updatedUser) public Object UpdateUser(User updatedUser)
{ {
var caller = GetCaller(); var session = GetSession();
using var db = Db.Connect();
if (caller == null || !db.IsParentOfChild(caller.Id, updatedUser) || !caller.HasWriteAccess) return session.Update(updatedUser)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); ? updatedUser
: _Unauthorized;
return db.UpdateUser(updatedUser);
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpPut($"{nameof(UpdateInstallation)}/")] [HttpPut($"{nameof(UpdateInstallation)}/")]
public Object UpdateInstallation(Installation installation) public Object UpdateInstallation(Installation installation)
{ {
var caller = GetCaller(); var session = GetSession();
if (caller is null || !caller.HasWriteAccess) return session.Update(installation)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); ? installation
: _Unauthorized;
using var db = Db.Connect();
var installationFromAccessibleInstallations = db
.GetAllAccessibleInstallations(caller)
.FirstOrDefault(i => i.Id == installation.Id);
if (installationFromAccessibleInstallations == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
// TODO: accessibility by other users etc
// TODO: sanity check changes
// foreach(var property in installationFromAccessibleInstallations.GetType().GetProperties()){
// if(installation.GetType().GetProperties().Contains(property))
// {
// property.SetValue(installationFromAccessibleInstallations, property.GetValue(installation));
// }
// }
return db.UpdateInstallation(installation);
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpPut($"{nameof(UpdateFolder)}/")] [HttpPut($"{nameof(UpdateFolder)}/")]
public Object UpdateFolder(Folder folder) public Object UpdateFolder(Folder folder)
{ {
var caller = GetCaller(); var session = GetSession();
if (caller is null || !caller.HasWriteAccess) return session.Update(folder)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); ? folder
: _Unauthorized;
using var db = Db.Connect();
var installationFromAccessibleFolders = db
.GetAllAccessibleFolders(caller)
.FirstOrDefault(f => f.Id == folder.Id);
if (installationFromAccessibleFolders == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
// TODO: accessibility by other users etc
// TODO: sanity check changes
// foreach(var property in installationFromAccessibleFolders.GetType().GetProperties()){
// if(folder.GetType().GetProperties().Contains(property))
// {
// property.SetValue(installationFromAccessibleFolders, property.GetValue(folder));
// }
// }
return db.UpdateFolder(installationFromAccessibleFolders);
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpDelete($"{nameof(DeleteUser)}/")] [HttpDelete($"{nameof(DeleteUser)}/")]
public Object DeleteUser(Int64 userId) public Object DeleteUser(Int64 userId)
{ {
var caller = GetCaller(); var session = GetSession();
var user = Db.GetUserById(userId);
if (caller is null || !caller.HasWriteAccess)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
var userToBeDeleted = db
.GetDescendantUsers(caller)
.FirstOrDefault(u => u.Id == userId);
if (userToBeDeleted is null) return session.Delete(user)
return new HttpResponseMessage(HttpStatusCode.Unauthorized); ? _Ok
: _Unauthorized;
return db.DeleteUser(userToBeDeleted);
} }
[Returns(HttpStatusCode.OK)] [Returns(OK)]
[Returns(HttpStatusCode.Unauthorized)] [Returns(Unauthorized)]
[Microsoft.AspNetCore.Mvc.HttpDelete($"{nameof(DeleteInstallation)}/")] [HttpDelete($"{nameof(DeleteInstallation)}/")]
public Object DeleteInstallation(Int64 installationId) public Object DeleteInstallation(Int64 installationId)
{ {
var caller = GetCaller(); var session = GetSession();
var installation = Db.GetInstallationById(installationId);
if (caller is null || !caller.HasWriteAccess)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
var installationToBeDeleted = db return session.Delete(installation)
.GetAllAccessibleInstallations(caller) ? _Ok
.FirstOrDefault(i => i.Id == installationId); : _Unauthorized;
if (installationToBeDeleted is null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
return db.DeleteInstallation(installationToBeDeleted);
} }
[ProducesResponseType(200)] [ProducesResponseType(200)]
[ProducesResponseType(401)] [ProducesResponseType(401)]
[Microsoft.AspNetCore.Mvc.HttpDelete($"{nameof(DeleteFolder)}/")] [HttpDelete($"{nameof(DeleteFolder)}/")]
public Object DeleteFolder(Int64 folderId) public Object DeleteFolder(Int64 folderId)
{ {
var caller = GetCaller(); var session = GetSession();
if (caller == null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
using var db = Db.Connect();
var folderToDelete = db var folder = Db.GetFolderById(folderId);
.GetAllAccessibleFolders(caller)
.FirstOrDefault(f => f.Id == folderId); return session.Delete(folder)
? _Ok
: _Unauthorized;
if (folderToDelete is null)
return new HttpResponseMessage(HttpStatusCode.Unauthorized);
return db.DeleteFolder(folderToDelete);
} }
private static User? GetCaller() private static Session? GetSession()
{ {
var ctxAccessor = new HttpContextAccessor(); var ctxAccessor = new HttpContextAccessor();
return ctxAccessor.HttpContext?.Items["User"] as User; return ctxAccessor.HttpContext?.Items["Session"] as Session;
} }
private static Boolean VerifyPassword(String password, User user)
{
var pwdBytes = Encoding.UTF8.GetBytes(password);
var saltBytes = Encoding.UTF8.GetBytes(user.Salt + "innovEnergy");
var pwdHash = Crypto.ComputeHash(pwdBytes, saltBytes);
return user.Password == pwdHash;
}
} }

View File

@ -1,6 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace InnovEnergy.App.Backend.Controllers;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public record Credentials(String Username, String Password);

View File

@ -0,0 +1,6 @@
using System.Diagnostics.CodeAnalysis;
namespace InnovEnergy.App.Backend.DataTypes;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public record Credentials(String Username, String Password);

View File

@ -0,0 +1,3 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class Folder : TreeNode {}

View File

@ -1,4 +1,4 @@
namespace InnovEnergy.App.Backend.Model; namespace InnovEnergy.App.Backend.DataTypes;
public class Installation : TreeNode public class Installation : TreeNode
@ -14,7 +14,6 @@ public class Installation : TreeNode
public Double Long { get; set; } public Double Long { get; set; }
public String S3Bucket { get; set; } = ""; public String S3Bucket { get; set; } = "";
public String? S3Key { get; set; } public String S3Key { get; set; } = "";
}
}

View File

@ -0,0 +1,25 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class CredentialsMethods
{
public static Session? Login(this Credentials credentials)
{
if (credentials.Username.IsNull() || credentials.Password.IsNull())
return null;
var user = Db.GetUserByEmail(credentials.Username);
if (user is null || !user.VerifyPassword(credentials.Password))
return null;
var session = new Session(user);
return Db.Create(session)
? session
: null;
}
}

View File

@ -0,0 +1,73 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class FolderMethods
{
public static IEnumerable<Folder> ChildFolders(this Folder parent)
{
return Db
.Folders
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<Installation> ChildInstallations(this Folder parent)
{
return Db
.Installations
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<Folder> DescendantFolders(this Folder parent)
{
return parent.Traverse(ChildFolders);
}
public static Boolean IsDescendantOf(this Folder folder, Folder ancestor)
{
return folder
.Ancestors()
.Any(u => u.Id == ancestor.Id);
}
public static IEnumerable<Folder> Ancestors(this Folder folder)
{
return folder.Unfold(Parent);
}
public static Folder? Parent(this Folder folder)
{
return IsAbsoluteRoot(folder)
? null
: Db.GetFolderById(folder.ParentId);
}
public static Boolean IsAbsoluteRoot(this Folder folder)
{
return folder.ParentId == 0; // root has ParentId 0 by definition
}
public static Boolean IsRelativeRoot(this Folder folder)
{
return folder.ParentId < 0; // root has ParentId 0 by definition
}
public static Boolean WasMoved(this Folder folder)
{
if (folder.IsRelativeRoot())
return false;
var existingFolder = Db.GetFolderById(folder.Id);
return existingFolder is not null
&& existingFolder.ParentId != folder.ParentId;
}
public static Boolean Exists(this Folder folder)
{
return Db.Folders.Any(f => f.Id == folder.Id);
}
}

View File

@ -0,0 +1,47 @@
using InnovEnergy.App.Backend.Database;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class InstallationMethods
{
public static IEnumerable<Folder> Ancestors(this Installation installation)
{
var parentFolder = Parent(installation);
return parentFolder is null
? Enumerable.Empty<Folder>()
: parentFolder.Ancestors();
}
public static Folder? Parent(this Installation installation)
{
return installation.IsRelativeRoot()
? null
: Db.GetFolderById(installation.ParentId);
}
public static Boolean IsRelativeRoot(this Installation i)
{
return i.ParentId < 0;
}
public static Boolean WasMoved(this Installation installation)
{
if (installation.IsRelativeRoot())
return false;
var existingInstallation = Db.GetInstallationById(installation.Id);
return existingInstallation is not null
&& existingInstallation.ParentId != installation.ParentId;
}
public static Boolean Exists(this Installation installation)
{
return Db.Installations.Any(i => i.Id == installation.Id);
}
}

View File

@ -0,0 +1,119 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class SessionMethods
{
public static Boolean Create(this Session? session, Folder? folder)
{
var user = session?.User;
return user is not null
&& folder is not null
&& user.HasWriteAccess
&& user.HasAccessTo(folder.Parent())
&& Db.Create(folder);
}
public static Boolean Update(this Session? session, Folder? folder)
{
var user = session?.User;
return user is not null
&& folder is not null
&& user.HasWriteAccess
&& user.HasAccessTo(folder)
&& (folder.IsRelativeRoot() || user.HasAccessTo(folder.Parent()))
&& Db.Update(folder);
}
public static Boolean Delete(this Session? session, Folder? folder)
{
var user = session?.User;
return user is not null
&& folder is not null
&& user.HasWriteAccess
&& user.HasAccessTo(folder) // TODO: && user.HasAccessTo(folder.Parent()) ???
&& Db.Delete(folder);
}
public static Boolean Create(this Session? session, Installation? installation)
{
var user = session?.User;
return user is not null
&& installation is not null
&& user.HasWriteAccess
&& user.HasAccessTo(installation.Parent())
&& Db.Create(installation);
}
public static Boolean Update(this Session? session, Installation? installation)
{
var user = session?.User;
return user is not null
&& installation is not null
&& user.HasWriteAccess
&& installation.Exists()
&& user.HasAccessTo(installation)
&& (installation.IsRelativeRoot() || user.HasAccessTo(installation.Parent())) // TODO: triple check this
&& Db.Update(installation);
}
public static Boolean Delete(this Session? session, Installation? installation)
{
var user = session?.User;
return user is not null
&& installation is not null
&& user.HasWriteAccess
&& user.HasAccessTo(installation) // TODO: && user.HasAccessTo(installation.Parent()) ???
&& Db.Delete(installation);
}
public static Boolean Create(this Session? session, User? newUser)
{
var sessionUser = session?.User;
if (sessionUser is null || newUser is null || !sessionUser.HasWriteAccess)
return false;
newUser.ParentId = sessionUser.Id; // Important!
return Db.Create(newUser);
}
public static Boolean Update(this Session? session, User? editedUser)
{
var sessionUser = session?.User;
return sessionUser is not null
&& editedUser is not null
&& sessionUser.HasWriteAccess
&& sessionUser.HasAccessTo(editedUser)
&& (editedUser.IsRelativeRoot() || sessionUser.HasAccessTo(editedUser.Parent())) // TODO: triple check this
&& Db.Update(editedUser);
}
public static Boolean Delete(this Session? session, User? userToDelete)
{
var sessionUser = session?.User;
return sessionUser is not null
&& userToDelete is not null
&& sessionUser.HasWriteAccess
&& sessionUser.HasAccessTo(userToDelete) // TODO: && user.HasAccessTo(installation.Parent()) ???
&& Db.Delete(userToDelete);
}
public static Boolean Logout(this Session? session)
{
return session is not null
&& Db.Sessions.Delete(s => s.Token == session.Token) > 0;
}
}

View File

@ -0,0 +1,342 @@
using System.Net.Http.Headers;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.Lib.Utils;
using Convert = System.Convert;
using static System.Text.Encoding;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class UserMethods
{
public static IEnumerable<Installation> AccessibleInstallations(this User user)
{
var direct = user.DirectlyAccessibleInstallations();
var fromFolders = user
.AccessibleFolders()
.SelectMany(u => u.ChildInstallations());
return direct
.Concat(fromFolders)
.Distinct();
}
public static IEnumerable<Folder> AccessibleFolders(this User user)
{
return user
.DirectlyAccessibleFolders()
.SelectMany(f => f.DescendantFolders())
.Distinct();
// Distinct because the user might have direct access
// to a child folder of a folder he has already access to
}
public static IEnumerable<TreeNode> AccessibleFoldersAndInstallations(this User user)
{
var folders = user.AccessibleFolders() as IEnumerable<TreeNode>;
var installations = user.AccessibleInstallations();
return folders.Concat(installations);
}
public static IEnumerable<Installation> DirectlyAccessibleInstallations(this User user)
{
return Db
.User2Installation
.Where(r => r.UserId == user.Id)
.Select(r => r.InstallationId)
.Select(Db.GetInstallationById)
.NotNull()
.Do(i => i.ParentId = -1); // hide inaccessible parents from calling user
}
public static IEnumerable<Folder> DirectlyAccessibleFolders(this User user)
{
return Db
.User2Folder
.Where(r => r.UserId == user.Id)
.Select(r => r.FolderId)
.Select(Db.GetFolderById)
.NotNull()
.Do(i => i.ParentId = -1); // hide inaccessible parents from calling user;
}
public static IEnumerable<User> ChildUsers(this User parent)
{
return Db
.Users
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<User> DescendantUsers(this User parent)
{
return parent.Traverse(ChildUsers);
}
public static Boolean IsDescendantOf(this User user, User ancestor)
{
return user
.Ancestors()
.Any(u => u.Id == ancestor.Id);
}
private static IEnumerable<User> Ancestors(this User user)
{
return user.Unfold(Parent);
}
public static Boolean VerifyPassword(this User user, String password)
{
return user.Password == user.SaltAndHashPassword(password);
}
public static String SaltAndHashPassword(this User user, String password)
{
var dataToHash = $"{password}{user.Salt()}";
return dataToHash
.Apply(UTF8.GetBytes)
.Apply(SHA256.HashData)
.Apply(Convert.ToBase64String);
}
public static User? Parent(this User u)
{
return u.IsAbsoluteRoot()
? null
: Db.GetUserById(u.ParentId);
}
public static Boolean IsAbsoluteRoot(this User u)
{
return u.ParentId == 0;
}
public static Boolean IsRelativeRoot(this User u)
{
return u.ParentId < 0;
}
public static Boolean HasDirectAccessTo(this User user, Folder folder)
{
return Db
.User2Folder
.Any(r => r.FolderId == folder.Id && r.UserId == user.Id);
}
public static Boolean HasAccessTo(this User user, Folder? folder)
{
if (folder is null)
return false;
return folder
.Ancestors()
.Any(user.HasDirectAccessTo);
}
public static Boolean HasDirectAccessTo(this User user, Installation installation)
{
return Db
.User2Installation
.Any(r => r.InstallationId == installation.Id && r.UserId == user.Id);
}
public static Boolean HasAccessTo(this User user, Installation? installation)
{
if (installation is null)
return false;
return user.HasDirectAccessTo(installation) ||
installation.Ancestors().Any(user.HasDirectAccessTo);
}
public static Boolean HasAccessTo(this User user, User? other)
{
if (other is null)
return false;
return other
.Ancestors()
.Skip(1) // Important! skip self, user cannot delete or edit himself
.Contains(user);
}
public static String Salt(this User user)
{
// + id => salt unique per user
// + InnovEnergy => globally unique
return $"{user.Id}InnovEnergy";
}
private static Byte[] HmacSha256Digest(String message, String secret)
{
// var encoding = new UTF8Encoding();
// var keyBytes = encoding.GetBytes(secret);
// var messageBytes = encoding.GetBytes(message);
// var cryptographer = new HMACSHA256(keyBytes);
// return cryptographer.ComputeHash(messageBytes);
var keyBytes = UTF8.GetBytes(secret);
var messageBytes = UTF8.GetBytes(message);
return HMACSHA256.HashData(keyBytes, messageBytes);
}
private static String BuildSignature(String method, String path, String data, Int64 time, String secret)
{
var messageToSign = "";
messageToSign += method + " /v2/" + path + "\n";
messageToSign += data + "\n";
// query strings
messageToSign += "\n";
// headers
messageToSign += "\n";
messageToSign += time;
Console.WriteLine("Message to sign:\n" + messageToSign);
var hmac = HmacSha256Digest(messageToSign, secret);
return Convert.ToBase64String(hmac);
}
// public Object CreateAndSaveUserS3ApiKey(User user)
// {
// //EXOSCALE API URL
// const String url = "https://api-ch-dk-2.exoscale.com/v2/";
// const String path = "access-key";
//
// //TODO HIDE ME
// const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
// const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
//
// var installationList = User2Installation
// .Where(i => i.UserId == user.Id)
// .SelectMany(i => Installations.Where(f => i.InstallationId == f.Id))
// .ToList();
//
//
// var instList = new JsonArray();
//
// foreach (var installation in installationList)
// {
// instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"});
// }
//
// var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList};
// var stringPayload = jsonPayload.ToJsonString();
//
// var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
// var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret);
//
// var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature;
//
// var client = new HttpClient();
// client.DefaultRequestHeaders.Authorization =
// new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader);
//
// var content = new StringContent(stringPayload, Encoding.UTF8, "application/json");
//
//
// var response = client.PostAsync(url+path, content).Result;
//
// if (response.StatusCode.ToString() != "OK")
// {
// return response;
// }
//
// var responseString = response.Content.ReadAsStringAsync().Result;
// return Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"'));
// // return SetUserS3ApiKey(user, newKey);
//
// }
public static Object CreateAndSaveInstallationS3ApiKey(Installation installation)
{
//EXOSCALE API URL
const String url = "https://api-ch-dk-2.exoscale.com/v2/";
const String path = "access-key";
//TODO HIDE ME
const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
var jsonPayload = new JsonObject
{
["name"] = installation.Id,
["operations"] = new JsonArray
{
"list-sos-bucket",
"get-sos-object"
},
["content"] = new JsonArray
{
new JsonObject
{
["domain"] = "sos",
["resource-name"] = installation.Name,
["resource-type"] = "bucket"
}
}
};
var stringPayload = jsonPayload.ToJsonString();
var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
var signature = BuildSignature("POST", path, stringPayload, unixExpiration, secret);
var authHeader = "credential=" + apiKey + ",expires=" + unixExpiration + ",signature=" + signature;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader);
var content = new StringContent(stringPayload, UTF8, "application/json");
var response = client.PostAsync(url + path, content).Result;
if (response.StatusCode.ToString() != "OK")
{
return response;
}
var responseString = response.Content.ReadAsStringAsync().Result;
var newKey = Regex
.Match(responseString, "key\\\":\\\"([A-Z])\\w+")
.ToString()
.Split('"')
.Last();
installation.S3Key = newKey;
Db.Update(installation);
return newKey;
}
// TODO
private static Boolean IsValidEmail(String email)
{
try
{
var emailAddress = new MailAddress(email);
}
catch
{
return false;
}
return true;
}
}

View File

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace InnovEnergy.App.Backend.DataTypes;
public abstract partial class TreeNode
{
public override Boolean Equals(Object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((TreeNode)obj);
}
protected Boolean Equals(TreeNode other) => Id == other.Id;
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override Int32 GetHashCode() => Id.GetHashCode();
public static Boolean operator ==(TreeNode? left, TreeNode? right) => Equals(left, right);
public static Boolean operator !=(TreeNode? left, TreeNode? right) => !Equals(left, right);
}

View File

@ -1,6 +1,6 @@
using SQLite; using SQLite;
namespace InnovEnergy.App.Backend.Model; namespace InnovEnergy.App.Backend.DataTypes;
public abstract partial class TreeNode public abstract partial class TreeNode
{ {
@ -12,10 +12,7 @@ public abstract partial class TreeNode
[Indexed] // parent/child relation [Indexed] // parent/child relation
public Int64 ParentId { get; set; } public Int64 ParentId { get; set; }
[Ignore] // not in DB, can be used in typescript as type discriminator [Ignore]
public String Type => GetType().Name; public String Type => GetType().Name;
[Ignore]
public IReadOnlyList<TreeNode>? Children { get; set; }
} }

View File

@ -1,17 +1,14 @@
using SQLite; using SQLite;
namespace InnovEnergy.App.Backend.Model; namespace InnovEnergy.App.Backend.DataTypes;
public class User : TreeNode public class User : TreeNode
{ {
[Indexed] [Indexed]
public String Email { get; set; } = null!; public String Email { get; set; } = null!;
public Boolean HasWriteAccess { get; set; } = false; public Boolean HasWriteAccess { get; set; } = false;
public String Salt { get; set; } = null!;
public String Language { get; set; } = null!; public String Language { get; set; } = null!;
public String Password { get; set; } = null!; public String Password { get; set; } = null!;
// TODO: must reset pwd // TODO: must reset pwd
} }

View File

@ -0,0 +1,38 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static Boolean Create(Installation installation)
{
// SQLite wrapper is smart and *modifies* t's Id to the one generated (autoincrement) by the insertion
return Connection.Insert(installation) > 0;
}
public static Boolean Create(Folder folder)
{
return Connection.Insert(folder) > 0;
}
public static Boolean Create(User user)
{
if (GetUserByEmail(user.Email) is not null) // TODO: User unique by username instead of email?
return false;
user.Password = user.SaltAndHashPassword(user.Password);
return Connection.Insert(user) > 0;
}
public static Boolean Create(Session session)
{
return Connection.Insert(session) > 0;
}
}

View File

@ -1,16 +1,20 @@
using System.Diagnostics.CodeAnalysis; using System.Reactive.Linq;
using InnovEnergy.App.Backend.Model; using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.Model.Relations; using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using SQLite; using SQLite;
namespace InnovEnergy.App.Backend.Database; namespace InnovEnergy.App.Backend.Database;
public static partial class Db public static partial class Db
{ {
internal const String DbPath = "./db.sqlite"; internal const String DbPath = "./db.sqlite";
public static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath); private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath);
public static TableQuery<Session> Sessions => Connection.Table<Session>(); public static TableQuery<Session> Sessions => Connection.Table<Session>();
public static TableQuery<Folder> Folders => Connection.Table<Folder>(); public static TableQuery<Folder> Folders => Connection.Table<Folder>();
@ -18,149 +22,55 @@ public static partial class Db
public static TableQuery<User> Users => Connection.Table<User>(); public static TableQuery<User> Users => Connection.Table<User>();
public static TableQuery<User2Folder> User2Folder => Connection.Table<User2Folder>(); public static TableQuery<User2Folder> User2Folder => Connection.Table<User2Folder>();
public static TableQuery<User2Installation> User2Installation => Connection.Table<User2Installation>(); public static TableQuery<User2Installation> User2Installation => Connection.Table<User2Installation>();
public static Int32 NbUser2Installation => User2Installation.Count();
public static Int32 NbUser2Folder => User2Folder.Count();
public static Int32 NbFolders => Folders.Count();
public static Int32 NbInstallations => Installations.Count();
public static Int32 NbUsers => Users.Count();
public static Folder? GetFolderById(Int64 id)
{
return Folders
.FirstOrDefault(f => f.Id == id);
}
public static Installation? GetInstallationById(Int64 id)
{
return Installations
.FirstOrDefault(i => i.Id == id);
}
public static User? GetUserById(Int64 id)
{
return Users
.FirstOrDefault(u => u.Id == id);
}
[SuppressMessage("ReSharper", "AccessToDisposedClosure")]
static Db() static Db()
{ {
// on startup create/migrate tables // on startup create/migrate tables
using var db = new SQLiteConnection(DbPath); Connection.RunInTransaction(() =>
db.RunInTransaction(() =>
{ {
db.CreateTable<User>(); Connection.CreateTable<User>();
db.CreateTable<Installation>(); Connection.CreateTable<Installation>();
db.CreateTable<Folder>(); Connection.CreateTable<Folder>();
db.CreateTable<User2Folder>(); Connection.CreateTable<User2Folder>();
db.CreateTable<User2Installation>(); Connection.CreateTable<User2Installation>();
db.CreateTable<Session>(); Connection.CreateTable<Session>();
}); });
var installation = Installations.First();
UserMethods.CreateAndSaveInstallationS3ApiKey(installation);
Observable.Interval(TimeSpan.FromDays(1))
.StartWith(0) // Do it right away (on startup)
.Subscribe(Cleanup); // and then daily
} }
// the C in CRUD
private static Int64 Create(TreeNode treeNode) private static Boolean RunTransaction(Func<Boolean> func)
{ {
var savepoint = Connection.SaveTransactionPoint();
var success = false;
try try
{ {
Connection.Insert(treeNode); success = func();
return SQLite3.LastInsertRowid(Connection.Handle);
} }
catch (Exception e) finally
{ {
return -1; if (success)
Connection.Release(savepoint);
else
Connection.RollbackTo(savepoint);
} }
return success;
} }
private static Boolean Create(Session session)
{
try
{
Connection.Insert(session);
return true;
}
catch (Exception e)
{
return false;
}
}
// the U in CRUD
private static Boolean Update(TreeNode treeNode)
{
try
{
Connection.InsertOrReplace(treeNode);
return true;
}
catch (Exception e)
{
return false;
}
}
// the D in CRUD
private static Boolean Delete(TreeNode treeNode)
{
try
{
Connection.Delete(treeNode);
return true;
}
catch (Exception e)
{
return false;
}
}
public static IEnumerable<Installation> GetAllAccessibleInstallations(User user)
{
var direct = GetDirectlyAccessibleInstallations(user);
var fromFolders = GetAllAccessibleFolders(user)
.SelectMany(GetChildInstallations);
return direct
.Concat(fromFolders)
.Distinct();
}
public static IEnumerable<Folder> GetAllAccessibleFolders(User user)
{
return GetDirectlyAccessibleFolders(user)
.SelectMany(GetDescendantFolders)
.Distinct();
// Distinct because the user might have direct access
// to a child folder of a folder he has already access to
}
public static IEnumerable<Installation> GetDirectlyAccessibleInstallations(User user)
{
return User2Installation
.Where(r => r.UserId == user.Id)
.Select(r => r.InstallationId)
.Select(GetInstallationById)
.NotNull()
.Do(i => i.ParentId = 0); // hide inaccessible parents from calling user
}
public static IEnumerable<Folder> GetDirectlyAccessibleFolders(User user)
{
return User2Folder
.Where(r => r.UserId == user.Id)
.Select(r => r.FolderId)
.Select(GetFolderById)
.NotNull()
.Do(i => i.ParentId = 0); // hide inaccessible parents from calling user;
}
public static Boolean AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId) public static Boolean AddToAccessibleInstallations(Int64 userId, Int64 updatedInstallationId)
{ {
@ -201,45 +111,22 @@ public static partial class Db
} }
public static User? GetUserByToken(String token) private static void Cleanup(Int64 _)
{ {
return Sessions DeleteS3Keys();
.Where(s => s.Token == token).ToList() DeleteStaleSessions();
.Where(s => s.Valid)
.Select(s => s.UserId)
.Select(GetUserById)
.FirstOrDefault();
} }
private static void DeleteStaleSessions()
public static Boolean NewSession(Session ses) => Create(ses);
public static Boolean DeleteSession(Int64 id)
{ {
try var deadline = DateTime.Now - Session.MaxAge;
{ Sessions.Delete(s => s.LastSeen < deadline);
Sessions.Delete(u => u.UserId == id);
return true;
}
catch (Exception e)
{
return false;
}
} }
public static String? GetInstallationS3Key(Int64 installationId) private static void DeleteS3Keys()
{ {
return Installations void DeleteKeys() => Installations.Do(i => i.S3Key = "").ForEach(Update); // TODO
.FirstOrDefault(i => i.Id == installationId)?
.S3Key; Connection.RunInTransaction(DeleteKeys);
}
public static void DeleteAllS3Keys()
{
foreach (var installation in Installations.ToList())
{
installation.S3Key = null;
Update(installation);
}
} }
} }

View File

@ -0,0 +1,63 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static Boolean Delete(Folder folder)
{
return RunTransaction(DeleteFolderAndAllItsDependencies);
Boolean DeleteFolderAndAllItsDependencies()
{
return folder
.DescendantFolders()
.All(DeleteDescendantFolderAndItsDependencies);
}
Boolean DeleteDescendantFolderAndItsDependencies(Folder f)
{
User2Folder .Delete(r => r.FolderId == f.Id);
Installations.Delete(r => r.ParentId == f.Id);
return Folders.Delete(r => r.Id == f.Id) > 0;
}
}
public static Boolean Delete(Installation installation)
{
return RunTransaction(DeleteInstallationAndItsDependencies);
Boolean DeleteInstallationAndItsDependencies()
{
User2Installation.Delete(i => i.InstallationId == installation.Id);
return Installations.Delete(i => i.Id == installation.Id) > 0;
}
}
public static Boolean Delete(User user)
{
return RunTransaction(DeleteUserAndHisDependencies);
Boolean DeleteUserAndHisDependencies()
{
User2Folder .Delete(u => u.UserId == user.Id);
User2Installation.Delete(u => u.UserId == user.Id);
return Users.Delete(u => u.Id == user.Id) > 0;
}
}
#pragma warning disable CS0618
// private!!
private static Boolean Delete(Session session)
{
return Sessions.Delete(s => s.Id == session.Id) > 0;
}
}

View File

@ -1,4 +1,4 @@
using InnovEnergy.App.Backend.Model.Relations; using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database; namespace InnovEnergy.App.Backend.Database;
@ -18,7 +18,7 @@ public static partial class Db
private static void CreateFakeUserTree() private static void CreateFakeUserTree()
{ {
foreach (var userId in Enumerable.Range(1, NbUsers)) foreach (var userId in Enumerable.Range(1, Users.Count()))
{ {
var user = GetUserById(userId); var user = GetUserById(userId);
if (user is null) if (user is null)
@ -34,7 +34,7 @@ public static partial class Db
private static void CreateFakeFolderTree() private static void CreateFakeFolderTree()
{ {
foreach (var folderId in Enumerable.Range(1, NbFolders)) foreach (var folderId in Enumerable.Range(1, Folders.Count()))
{ {
var folder = GetFolderById(folderId); var folder = GetFolderById(folderId);
if (folder is null) if (folder is null)
@ -50,7 +50,7 @@ public static partial class Db
private static void LinkFakeInstallationsToFolders() private static void LinkFakeInstallationsToFolders()
{ {
var nFolders = NbFolders; var nFolders = Folders.Count();
foreach (var installation in Installations) foreach (var installation in Installations)
{ {
@ -64,8 +64,8 @@ public static partial class Db
foreach (var uf in User2Folder) // remove existing relations foreach (var uf in User2Folder) // remove existing relations
Connection.Delete(uf); Connection.Delete(uf);
var nFolders = NbFolders; var nFolders = Folders.Count();
var nUsers = NbUsers; var nUsers = Users.Count();
foreach (var user in Users) foreach (var user in Users)
while (Random.Shared.Next((Int32)(nUsers - user.Id + 1)) != 0) while (Random.Shared.Next((Int32)(nUsers - user.Id + 1)) != 0)
@ -84,7 +84,7 @@ public static partial class Db
foreach (var ui in User2Installation) // remove existing relations foreach (var ui in User2Installation) // remove existing relations
Connection.Delete(ui); Connection.Delete(ui);
var nbInstallations = NbInstallations; var nbInstallations = Installations.Count();
foreach (var user in Users) foreach (var user in Users)
while (Random.Shared.Next(5) != 0) while (Random.Shared.Next(5) != 0)

View File

@ -1,91 +0,0 @@
using InnovEnergy.App.Backend.Model;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static IEnumerable<Folder> GetChildFolders(this Folder parent)
{
return Folders.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<Installation> GetChildInstallations(this Folder parent)
{
return Installations.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<Folder> GetDescendantFolders(this Folder parent)
{
return parent.Traverse(GetChildFolders);
}
public static Boolean IsDescendantOf(this Folder folder, Int64 ancestorFolderId)
{
return Ancestors(folder)
.Any(u => u.Id == ancestorFolderId);
}
public static Boolean IsDescendantOf(this Folder folder, Folder ancestor)
{
return IsDescendantOf(folder, ancestor.Id);
}
private static IEnumerable<Folder> Ancestors(this Folder child)
{
return child.Unfold(GetParent);
}
public static Folder? GetParent(this Folder f)
{
return IsRoot(f)
? null
: GetFolderById(f.ParentId);
}
public static Boolean IsRoot(this Folder f)
{
return f.ParentId == 0; // root has ParentId 0 by definition
}
public static Int64 CreateFolder(Folder folder)
{
return Create(folder);
}
public static Boolean UpdateFolder(Folder folder)
{
// TODO: no circles in path
return Update(folder);
}
// These should not be necessary, just Update folder/installation with new parentId
// public Boolean ChangeParent(Installation child, Int64 parentId)
// {
// child.ParentId = parentId;
// return UpdateInstallation(child);
// }
//
// public Boolean ChangeParent(Folder child, Int64 parentId)
// {
// child.ParentId = parentId;
// return UpdateFolder(child);
// }
public static Boolean DeleteFolder(Folder folder)
{
// Delete direct children
User2Folder .Delete(f => f.FolderId == folder.Id);
Installations.Delete(i => i.ParentId == folder.Id);
// recursion
Folders.Where(f => f.ParentId == folder.Id)
.ForEach(DeleteFolder);
return Delete(folder);
}
}

View File

@ -1,47 +0,0 @@
using InnovEnergy.App.Backend.Model;
using SQLite;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static IEnumerable<Folder> Ancestors(this Installation installation)
{
var parentFolder = GetParent(installation);
return parentFolder is null
? Enumerable.Empty<Folder>()
: Ancestors(parentFolder);
}
public static Folder? GetParent(this Installation installation)
{
return IsRoot(installation)
? null
: GetFolderById(installation.ParentId);
}
public static Boolean IsRoot(this Installation i)
{
return i.ParentId == 0; // root has ParentId 0 by definition
}
public static Int64 CreateInstallation(this Installation installation)
{
return Create(installation);
}
public static Boolean UpdateInstallation(this Installation installation)
{
return Update(installation);
}
public static Boolean DeleteInstallation(this Installation installation)
{
User2Installation.Delete(i => i.InstallationId == installation.Id);
return Delete(installation);
}
}

View File

@ -0,0 +1,86 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static Folder? GetFolderById(Int64 id)
{
return Folders
.FirstOrDefault(f => f.Id == id);
}
public static Installation? GetInstallationById(Int64 id)
{
return Installations
.FirstOrDefault(i => i.Id == id);
}
public static User? GetUserById(Int64 id)
{
return Users
.FirstOrDefault(u => u.Id == id);
}
// private!!
private static Session? GetSessionById(Int64 id)
{
#pragma warning disable CS0618
return Sessions
.FirstOrDefault(u => u.Id == id);
#pragma warning restore CS0618
}
public static User? GetUserByEmail(String email)
{
return Users
.FirstOrDefault(u => u.Email == email);
}
public static Session? GetSession(String token)
{
var session = Sessions
.FirstOrDefault(s => s.Token == token);
// cannot use session.Valid in the DB query above.
// It does not exist in the db (IgnoreAttribute)
if (session is null)
return null;
if (!session.Valid)
{
Delete(session);
return null;
}
return session;
}
public static User? GetUserBySessionToken(String token)
{
var session = Sessions
.FirstOrDefault(s => s.Token == token);
// cannot user session.Expired in the DB query above.
// It does not exist in the db (IgnoreAttribute)
if (session is null)
return null;
if (!session.Valid)
{
Delete(session);
return null;
}
return GetUserById(session.UserId);
}
}

View File

@ -0,0 +1,64 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static Boolean Update(Folder folder)
{
if (folder.IsRelativeRoot()) // TODO: triple check
{
var original = GetFolderById(folder.Id);
if (original is null)
return false;
folder.ParentId = original.ParentId;
}
return Connection.InsertOrReplace(folder) > 0;
}
public static Boolean Update(Installation installation)
{
if (installation.IsRelativeRoot()) // TODO: triple check
{
var original = GetInstallationById(installation.Id);
if (original is null)
return false;
installation.ParentId = original.ParentId;
}
return Connection.InsertOrReplace(installation) > 0;
}
public static Boolean Update(User user)
{
var originalUser = GetUserById(user.Id);
return originalUser is not null
&& user.Id == originalUser.Id // these columns must not be modified!
&& user.ParentId == originalUser.ParentId
&& user.Email == originalUser.Email
&& user.Password == originalUser.Password
&& Connection.InsertOrReplace(user) > 0;
}
public static Boolean Update(this Session session)
{
#pragma warning disable CS0618
var originalSession = GetSessionById(session.Id);
#pragma warning restore CS0618
return originalSession is not null
&& session.Token == originalSession.Token // these columns must not be modified!
&& session.UserId == originalSession.UserId
&& Connection.InsertOrReplace(session) > 0;
}
}

View File

@ -1,254 +0,0 @@
using System.Net.Http.Headers;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using InnovEnergy.App.Backend.Model;
using InnovEnergy.App.Backend.Utils;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
public static IEnumerable<User> GetChildUsers(this User parent)
{
return Users
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<User> GetDescendantUsers(this User parent)
{
return parent.Traverse(GetChildUsers);
}
public static Boolean IsDescendantOf(this User user, User ancestor)
{
return Ancestors(user)
.Any(u => u.Id == ancestor.Id);
}
private static IEnumerable<User> Ancestors(this User child)
{
return child.Unfold(GetParent);
}
public static User? GetParent(this User u)
{
return IsRoot(u)
? null
: GetUserById(u.ParentId);
}
public static Boolean IsRoot(this User u)
{
return u.ParentId == 0; // root has ParentId 0 by definition
}
public static Boolean HasDirectAccessToFolder(this User user, Folder folder)
{
return HasDirectAccessToFolder(user, folder.Id);
}
public static Boolean HasDirectAccessToFolder(this User user, Int64 folderId)
{
return User2Folder.Any(r => r.FolderId == folderId && r.UserId == user.Id);
}
public static Boolean HasAccessToFolder(this User user, Int64 folderId)
{
var folder = GetFolderById(folderId);
if (folder is null)
return false;
return Ancestors(folder).Any(f => HasDirectAccessToFolder(user, f));
}
public static User? GetUserByEmail(String email) => Users.FirstOrDefault(u => u.Email == email);
public static Int64 CreateUser(User user)
{
if (GetUserByEmail(user.Email) is not null)
return -1; // TODO: User with that email already exists
//Salting and Hashing password
var salt = Crypto.GenerateSalt();
var hashedPassword = Crypto.ComputeHash(Encoding.UTF8.GetBytes(user.Password),
Encoding.UTF8.GetBytes(salt + "innovEnergy"));
user.Salt = salt;
user.Password = hashedPassword;
return Create(user);
}
private static Byte[] HmacSha256Digest(String message, String secret)
{
var encoding = new UTF8Encoding();
var keyBytes = encoding.GetBytes(secret);
var messageBytes = encoding.GetBytes(message);
var cryptographer = new HMACSHA256(keyBytes);
return cryptographer.ComputeHash(messageBytes);
}
private static String BuildSignature(String method, String path, String data, Int64 time, String secret)
{
var messageToSign = "";
messageToSign += method + " /v2/" + path + "\n";
messageToSign += data + "\n";
// query strings
messageToSign += "\n";
// headers
messageToSign += "\n";
messageToSign += time;
Console.WriteLine("Message to sign:\n" + messageToSign);
var hmac = HmacSha256Digest(messageToSign, secret);
return Convert.ToBase64String(hmac);
}
// public Object CreateAndSaveUserS3ApiKey(User user)
// {
// //EXOSCALE API URL
// const String url = "https://api-ch-dk-2.exoscale.com/v2/";
// const String path = "access-key";
//
// //TODO HIDE ME
// const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
// const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
//
// var installationList = User2Installation
// .Where(i => i.UserId == user.Id)
// .SelectMany(i => Installations.Where(f => i.InstallationId == f.Id))
// .ToList();
//
//
// var instList = new JsonArray();
//
// foreach (var installation in installationList)
// {
// instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"});
// }
//
// var jsonPayload = new JsonObject { ["name"] = user.Email, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList};
// var stringPayload = jsonPayload.ToJsonString();
//
// var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
// var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret);
//
// var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature;
//
// var client = new HttpClient();
// client.DefaultRequestHeaders.Authorization =
// new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader);
//
// var content = new StringContent(stringPayload, Encoding.UTF8, "application/json");
//
//
// var response = client.PostAsync(url+path, content).Result;
//
// if (response.StatusCode.ToString() != "OK")
// {
// return response;
// }
//
// var responseString = response.Content.ReadAsStringAsync().Result;
// return Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"'));
// // return SetUserS3ApiKey(user, newKey);
//
// }
public static Object CreateAndSaveInstallationS3ApiKey(Installation installation)
{
//EXOSCALE API URL
const String url = "https://api-ch-dk-2.exoscale.com/v2/";
const String path = "access-key";
//TODO HIDE ME
const String secret = "S2K1okphiCSNK4mzqr4swguFzngWAMb1OoSlZsJa9F0";
const String apiKey = "EXOb98ec9008e3ec16e19d7b593";
var instList = new JsonArray();
instList.Add(new JsonObject {["domain"] = "sos",["resource-name"] = installation.Name,["resource-type"] = "bucket"});
var jsonPayload = new JsonObject { ["name"] = installation.Id, ["operations"] = new JsonArray{ "list-sos-bucket", "get-sos-object" }, ["content"] = instList};
var stringPayload = jsonPayload.ToJsonString();
var unixExpiration = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var signature = BuildSignature("POST", path, stringPayload, unixExpiration , secret);
var authHeader = "credential="+apiKey+",expires="+unixExpiration+",signature="+signature;
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authHeader);
var content = new StringContent(stringPayload, Encoding.UTF8, "application/json");
var response = client.PostAsync(url+path, content).Result;
if (response.StatusCode.ToString() != "OK")
{
return response;
}
var responseString = response.Content.ReadAsStringAsync().Result;
var newKey = Enumerable.Last(Regex.Match(responseString, "key\\\":\\\"([A-Z])\\w+").ToString().Split('"'));
installation.S3Key = newKey;
UpdateInstallation(installation);
return newKey;
}
public static Boolean UpdateUser(User user)
{
var oldUser = GetUserById(user.Id);
if (oldUser == null)
return false; // TODO: "User doesn't exist"
//Checking for unchangeable things
// TODO: depends on privileges of caller
user.Id = oldUser.Id;
user.ParentId = oldUser.ParentId;
user.Email = oldUser.Email;
return Update(user);
}
public static Boolean DeleteUser(User user)
{
User2Folder.Delete(u => u.UserId == user.Id);
User2Installation.Delete(u => u.UserId == user.Id);
//Todo check for orphaned Installations/Folders
// GetChildUsers()
return Delete(user);
}
// TODO
private static Boolean IsValidEmail(String email)
{
try
{
var emailAddress = new MailAddress(email);
}
catch
{
return false;
}
return true;
}
}

View File

@ -1,6 +0,0 @@
namespace InnovEnergy.App.Backend.Model;
public class Folder : TreeNode
{
}

View File

@ -1,35 +0,0 @@
using SQLite;
namespace InnovEnergy.App.Backend.Model.Relations;
public class Session : Relation<String, Int64>
{
[Indexed] public String Token { get => Left ; set => Left = value;}
[Indexed] public Int64 UserId { get => Right; set => Right = value;}
[Indexed] public DateTime ExpiresAt { get; set; }
[Ignore] public Boolean Valid => ExpiresAt > DateTime.Now;
[Ignore] public Boolean Expired => !Valid;
[Obsolete("To be used only by serializer")]
public Session()
{}
public Session(User user) : this(user, TimeSpan.FromDays(7))
{
}
public Session(User user, TimeSpan validFor)
{
Token = CreateToken();
UserId = user.Id;
ExpiresAt = DateTime.Now + validFor;
}
private static String CreateToken()
{
var token = new Byte[16]; // 128 bit
Random.Shared.NextBytes(token);
return Convert.ToBase64String(token);
}
}

View File

@ -1,9 +0,0 @@
using SQLite;
namespace InnovEnergy.App.Backend.Model.Relations;
internal class User2Folder : Relation<Int64, Int64>
{
[Indexed] public Int64 UserId { get => Left ; set => Left = value;}
[Indexed] public Int64 FolderId { get => Right; set => Right = value;}
}

View File

@ -1,9 +0,0 @@
using SQLite;
namespace InnovEnergy.App.Backend.Model.Relations;
internal class User2Installation : Relation<Int64, Int64>
{
[Indexed] public Int64 UserId { get => Left ; set => Left = value;}
[Indexed] public Int64 InstallationId { get => Right; set => Right = value;}
}

View File

@ -1,22 +0,0 @@
using System.Diagnostics.CodeAnalysis;
namespace InnovEnergy.App.Backend.Model;
public abstract partial class TreeNode
{
// Note: Only consider Id, but not ParentId for TreeNode equality checks
protected Boolean Equals(TreeNode other)
{
return Id == other.Id;
}
public override Boolean Equals(Object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((TreeNode)obj);
}
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override Int32 GetHashCode() => Id.GetHashCode();
}

View File

@ -8,11 +8,9 @@ public static class Program
{ {
public static void Main(String[] args) public static void Main(String[] args)
{ {
using (var db = Db.Connect())
db.CreateFakeRelations();
Observable.Interval(TimeSpan.FromDays(1)).Subscribe((_) => deleteInstallationS3KeysDaily());
Db.CreateFakeRelations();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); // TODO: remove magic, specify controllers explicitly builder.Services.AddControllers(); // TODO: remove magic, specify controllers explicitly
@ -47,22 +45,19 @@ public static class Program
app.Run(); app.Run();
} }
private static void deleteInstallationS3KeysDaily()
{
using var db = Db.Connect();
db.DeleteS3KeysDaily();
}
private static async Task SetSessionUser(HttpContext ctx, RequestDelegate next) private static async Task SetSessionUser(HttpContext ctx, RequestDelegate next)
{ {
var headers = ctx.Request.Headers; var headers = ctx.Request.Headers;
var hasToken = headers.TryGetValue("auth", out var token); var hasToken = headers.TryGetValue("auth", out var token) ;
if (hasToken) if (hasToken)
{ {
using var db = Db.Connect(); var session = Db.GetSession(token);
ctx.Items["User"] = db.GetUserByToken(token.ToString());
if (session is not null)
ctx.Items["User"] = session;
} }
await next(ctx); await next(ctx);

View File

@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using SQLite; using SQLite;
namespace InnovEnergy.App.Backend.Model.Relations; namespace InnovEnergy.App.Backend.Relations;
public abstract class Relation<L,R> public abstract class Relation<L,R>
{ {

View File

@ -0,0 +1,44 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.Lib.Utils;
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class Session : Relation<String, Int64>
{
public static TimeSpan MaxAge { get; } = TimeSpan.FromDays(7);
[Unique ] public String Token { get => Left ; init => Left = value;}
[Indexed] public Int64 UserId { get => Right; init => Right = value;}
[Indexed] public DateTime LastSeen { get; set; }
[Ignore] public Boolean Valid => DateTime.Now - LastSeen < MaxAge
&& !User.IsNull();
[Ignore] public User User => _User ??= Db.GetUserById(UserId)!;
private User? _User;
[Obsolete("To be used only by deserializer")]
public Session()
{}
public Session(User user)
{
_User = user;
Token = CreateToken();
UserId = user.Id;
LastSeen = DateTime.Now;
}
private static String CreateToken()
{
var token = new Byte[24];
Random.Shared.NextBytes(token);
return Convert.ToBase64String(token);
}
}

View File

@ -0,0 +1,9 @@
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class User2Folder : Relation<Int64, Int64>
{
[Indexed] public Int64 UserId { get => Left ; init => Left = value;}
[Indexed] public Int64 FolderId { get => Right; init => Right = value;}
}

View File

@ -0,0 +1,9 @@
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class User2Installation : Relation<Int64, Int64>
{
[Indexed] public Int64 UserId { get => Left ; init => Left = value;}
[Indexed] public Int64 InstallationId { get => Right; init => Right = value;}
}

View File

@ -1,22 +0,0 @@
using System.Security.Cryptography;
namespace InnovEnergy.App.Backend.Utils;
public static class Crypto
{
public static String ComputeHash(Byte[] bytesToHash, Byte[] salt)
{
using var mySHA256 = SHA256.Create();
var hashValue = mySHA256.ComputeHash(bytesToHash);
// var hashValue = new Rfc2898DeriveBytes(hashValue, salt, 10000);
return Convert.ToBase64String(hashValue);
}
public static String GenerateSalt()
{
var bytes = new Byte[128 / 8];
var rng = RandomNumberGenerator.Create();
rng.GetBytes(bytes);
return Convert.ToBase64String(bytes);
}
}

Binary file not shown.