Updated backend

This commit is contained in:
Noe 2023-09-15 13:34:28 +02:00
parent d9548f6207
commit 637c8ae4ba
29 changed files with 749 additions and 67 deletions

View File

@ -13,6 +13,7 @@
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.3" /> <PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.3" />
<PackageReference Include="Microsoft.AspNet.Identity.Owin" Version="2.2.3" /> <PackageReference Include="Microsoft.AspNet.Identity.Owin" Version="2.2.3" />
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.9" /> <PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="6.0.21" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
@ -39,6 +40,200 @@
<None Update="Resources/s3cmd.py"> <None Update="Resources/s3cmd.py">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Remove="DbBackups\db-1692621279.sqlite" />
<None Remove="DbBackups\db-1692620662.sqlite" />
<None Remove="DbBackups\db-1692620973.sqlite" />
<None Remove="DbBackups\db-1692620165.sqlite" />
<None Remove="DbBackups\db-1692620475.sqlite" />
<None Remove="DbBackups\db-1692619622.sqlite" />
<None Remove="DbBackups\db-1692620266.sqlite" />
<None Remove="DbBackups\db-1692618414.sqlite" />
<None Remove="DbBackups\db-1692618326.sqlite" />
<None Remove="DbBackups\db-1692612258.sqlite" />
<None Remove="DbBackups\db-1692619963.sqlite" />
<None Remove="DbBackups\db-1692620296.sqlite" />
<None Remove="DbBackups\db-1692618125.sqlite" />
<None Remove="DbBackups\db-1692621869.sqlite" />
<None Remove="DbBackups\db-1692621699.sqlite" />
<None Remove="DbBackups\db-1692611631.sqlite" />
<None Remove="DbBackups\db-1692627958.sqlite" />
<None Remove="DbBackups\db-1692715302.sqlite" />
<None Remove="DbBackups\db-1692715647.sqlite" />
<None Remove="DbBackups\db-1692715652.sqlite" />
<None Remove="DbBackups\db-1692884061.sqlite" />
<None Remove="DbBackups\db-1692884224.sqlite" />
<None Remove="DbBackups\db-1692884244.sqlite" />
<None Remove="DbBackups\db-1692884524.sqlite" />
<None Remove="DbBackups\db-1692884642.sqlite" />
<None Remove="DbBackups\db-1692885117.sqlite" />
<None Remove="DbBackups\db-1692885781.sqlite" />
<None Remove="DbBackups\db-1692885908.sqlite" />
<None Remove="DbBackups\db-1692889590.sqlite" />
<None Remove="DbBackups\db-1692889640.sqlite" />
<None Remove="DbBackups\db-1692890673.sqlite" />
<None Remove="DbBackups\db-1692890981.sqlite" />
<None Remove="DbBackups\db-1692891081.sqlite" />
<None Remove="DbBackups\db-1692891207.sqlite" />
<None Remove="DbBackups\db-1692891239.sqlite" />
<None Remove="DbBackups\db-1692891540.sqlite" />
<None Remove="DbBackups\db-1692891640.sqlite" />
<None Remove="DbBackups\db-1692947517.sqlite" />
<None Remove="DbBackups\db-1692951226.sqlite" />
<None Remove="DbBackups\db-1692956795.sqlite" />
<None Remove="DbBackups\db-1692957809.sqlite" />
<None Remove="DbBackups\db-1692958545.sqlite" />
<None Remove="DbBackups\db-1692965093.sqlite" />
<None Remove="DbBackups\db-1692965087.sqlite" />
<None Remove="DbBackups\db-1692965105.sqlite" />
<None Remove="DbBackups\db-1692965660.sqlite" />
<None Remove="DbBackups\db-1692965676.sqlite" />
<None Remove="DbBackups\db-1693813301.sqlite" />
<None Remove="DbBackups\db-1693815606.sqlite" />
<None Remove="DbBackups\db-1693813151.sqlite" />
<None Remove="DbBackups\db-1693388471.sqlite" />
<None Remove="DbBackups\db-1693385306.sqlite" />
<None Remove="DbBackups\db-1693236515.sqlite" />
<None Remove="DbBackups\db-1693230148.sqlite" />
<None Remove="DbBackups\db-1693301572.sqlite" />
<None Remove="DbBackups\db-1693388748.sqlite" />
<None Remove="DbBackups\db-1693388189.sqlite" />
<None Remove="DbBackups\db-1693297768.sqlite" />
<None Remove="DbBackups\db-1693299939.sqlite" />
<None Remove="DbBackups\db-1693229044.sqlite" />
<None Remove="DbBackups\db-1693225598.sqlite" />
<None Remove="DbBackups\db-1693300771.sqlite" />
<None Remove="DbBackups\db-1693212155.sqlite" />
<None Remove="DbBackups\db-1693225325.sqlite" />
<None Remove="DbBackups\db-1693212119.sqlite" />
<None Remove="DbBackups\db-1693211833.sqlite" />
<None Remove="DbBackups\db-1693392099.sqlite" />
<None Remove="DbBackups\db-1693392147.sqlite" />
<None Remove="DbBackups\db-1693391476.sqlite" />
<None Remove="DbBackups\db-1693395914.sqlite" />
<None Remove="DbBackups\db-1693394644.sqlite" />
<None Remove="DbBackups\db-1693389535.sqlite" />
<None Remove="DbBackups\db-1693389069.sqlite" />
<None Remove="DbBackups\db-1693812839.sqlite" />
<None Remove="DbBackups\db-1693389121.sqlite" />
<None Remove="DbBackups\db-1693390948.sqlite" />
<None Remove="DbBackups\db-1693401522.sqlite" />
<None Remove="DbBackups\db-1693299943.sqlite" />
<None Remove="DbBackups\db-1693230582.sqlite" />
<None Remove="DbBackups\db-1693388417.sqlite" />
<None Remove="DbBackups\db-1693238297.sqlite" />
<None Remove="DbBackups\db-1693228621.sqlite" />
<None Remove="DbBackups\db-1693388588.sqlite" />
<None Remove="DbBackups\db-1693823647.sqlite" />
<None Remove="DbBackups\db-1693390865.sqlite" />
<None Remove="DbBackups\db-1693395143.sqlite" />
<None Remove="DbBackups\db-1693823298.sqlite" />
<None Remove="DbBackups\db-1693389080.sqlite" />
<None Remove="DbBackups\db-1693390583.sqlite" />
<None Remove="DbBackups\db-1693391825.sqlite" />
<None Remove="DbBackups\db-1693391007.sqlite" />
<None Remove="DbBackups\db-1693390368.sqlite" />
<None Remove="DbBackups\db-1693391131.sqlite" />
<None Remove="DbBackups\db-1693392465.sqlite" />
<None Remove="DbBackups\db-1693820540.sqlite" />
<None Remove="DbBackups\db-1693389837.sqlite" />
<None Remove="DbBackups\db-1693394858.sqlite" />
<None Remove="DbBackups\db-1693395207.sqlite" />
<None Remove="DbBackups\db-1693815792.sqlite" />
<None Remove="DbBackups\db-1693389713.sqlite" />
<None Remove="DbBackups\db-1693391988.sqlite" />
<None Remove="DbBackups\db-1693389959.sqlite" />
<None Remove="DbBackups\db-1693395856.sqlite" />
<None Remove="DbBackups\db-1693488424.sqlite" />
<None Remove="DbBackups\db-1693474277.sqlite" />
<None Remove="DbBackups\db-1693482868.sqlite" />
<None Remove="DbBackups\db-1693472646.sqlite" />
<None Remove="DbBackups\db-1693470245.sqlite" />
<None Remove="DbBackups\db-1693470249.sqlite" />
<None Remove="DbBackups\db-1693469782.sqlite" />
<None Remove="DbBackups\db-1693497578.sqlite" />
<None Remove="DbBackups\db-1693474069.sqlite" />
<None Remove="DbBackups\db-1693499184.sqlite" />
<None Remove="DbBackups\db-1693395013.sqlite" />
<None Remove="DbBackups\db-1693398191.sqlite" />
<None Remove="DbBackups\db-1693394891.sqlite" />
<None Remove="DbBackups\db-1693390733.sqlite" />
<None Remove="DbBackups\db-1693389451.sqlite" />
<None Remove="DbBackups\db-1693390182.sqlite" />
<None Remove="DbBackups\db-1693811965.sqlite" />
<None Remove="DbBackups\db-1692966481.sqlite" />
<None Remove="DbBackups\db-1692969227.sqlite" />
<None Remove="DbBackups\db-1692966381.sqlite" />
<None Remove="DbBackups\db-1692967853.sqlite" />
<None Remove="DbBackups\db-1693820304.sqlite" />
<None Remove="DbBackups\db-1692967068.sqlite" />
<None Remove="DbBackups\db-1693822619.sqlite" />
<None Remove="DbBackups\db-1692969899.sqlite" />
<None Remove="DbBackups\db-1693820664.sqlite" />
<None Remove="DbBackups\db-1692966501.sqlite" />
<None Remove="DbBackups\db-1693820595.sqlite" />
<None Remove="DbBackups\db-1692966495.sqlite" />
<None Remove="DbBackups\db-1692967782.sqlite" />
<None Remove="DbBackups\db-1693820327.sqlite" />
<None Remove="DbBackups\db-1692966486.sqlite" />
<None Remove="DbBackups\db-1692965827.sqlite" />
<None Remove="DbBackups\db-1693581684.sqlite" />
<None Remove="DbBackups\db-1692966088.sqlite" />
<None Remove="DbBackups\db-1693207198.sqlite" />
<None Remove="DbBackups\db-1693214346.sqlite" />
<None Remove="DbBackups\db-1692979283.sqlite" />
<None Remove="DbBackups\db-1692979039.sqlite" />
<None Remove="DbBackups\db-1692979326.sqlite" />
<None Remove="DbBackups\db-1693210467.sqlite" />
<None Remove="DbBackups\db-1692970330.sqlite" />
<None Remove="DbBackups\db-1692979087.sqlite" />
<None Remove="DbBackups\db-1692971615.sqlite" />
<None Remove="DbBackups\db-1692970282.sqlite" />
<None Remove="DbBackups\db-1692966463.sqlite" />
<None Remove="DbBackups\db-1692971631.sqlite" />
<None Remove="DbBackups\db-1692967061.sqlite" />
<None Remove="DbBackups\db-1692968130.sqlite" />
<None Remove="DbBackups\db-1692969863.sqlite" />
<None Remove="DbBackups\db-1693574723.sqlite" />
<None Remove="DbBackups\db-1692969234.sqlite" />
<None Remove="DbBackups\db-1693574756.sqlite" />
<None Remove="DbBackups\db-1693572655.sqlite" />
<None Remove="DbBackups\db-1693572839.sqlite" />
<None Remove="DbBackups\db-1693563487.sqlite" />
<None Remove="DbBackups\db-1693571800.sqlite" />
<None Remove="DbBackups\db-1693554456.sqlite" />
<None Remove="DbBackups\db-1693571859.sqlite" />
<None Remove="DbBackups\db-1693571699.sqlite" />
<None Remove="DbBackups\db-1693571694.sqlite" />
<None Remove="DbBackups\db-1693555209.sqlite" />
<None Remove="DbBackups\db-1693571639.sqlite" />
<None Remove="DbBackups\db-1693465491.sqlite" />
<None Remove="DbBackups\db-1693571738.sqlite" />
<None Remove="DbBackups\db-1693465474.sqlite" />
<None Remove="DbBackups\db-1693498493.sqlite" />
<None Remove="DbBackups\db-1693575085.sqlite" />
<None Remove="DbBackups\db-1693574598.sqlite" />
<None Remove="DbBackups\db-1693575136.sqlite" />
<None Remove="DbBackups\db-1693574755.sqlite" />
<None Remove="DbBackups\db-1693574962.sqlite" />
<None Remove="DbBackups\db-1693822770.sqlite" />
<None Remove="DbBackups\db-1693822650.sqlite" />
<None Remove="DbBackups\db-1693822757.sqlite" />
<None Remove="DbBackups\db-1693822641.sqlite" />
<None Remove="DbBackups\db-1693823785.sqlite" />
<None Remove="DbBackups\db-1693823723.sqlite" />
<None Remove="DbBackups\db-1693836773.sqlite" />
<None Remove="DbBackups\db-1693837575.sqlite" />
<None Remove="DbBackups\db-1693837906.sqlite" />
<None Remove="DbBackups\db-1693838013.sqlite" />
<None Remove="DbBackups\db-1693837691.sqlite" />
<None Remove="DbBackups\db-1693837666.sqlite" />
<None Remove="DbBackups\db-1693838039.sqlite" />
<None Remove="DbBackups\db-1693838563.sqlite" />
<None Remove="DbBackups\db-1693838248.sqlite" />
<None Remove="DbBackups\db-1693838578.sqlite" />
<None Remove="DbBackups\db-1693839416.sqlite" />
<None Remove="DbBackups\db-1693839492.sqlite" />
<None Remove="DbBackups\db-1694156276.sqlite" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -197,7 +197,10 @@ public class Controller : ControllerBase
if (user == null) if (user == null)
return Unauthorized(); return Unauthorized();
return user.DescendantUsers().Select(u => u.HidePassword()).ToList(); return user
.DescendantUsers()
.Select(u => u.HidePassword())
.ToList();
} }
@ -252,7 +255,7 @@ public class Controller : ControllerBase
[HttpPost(nameof(CreateUser))] [HttpPost(nameof(CreateUser))]
public ActionResult<User> CreateUser(User newUser, Token authToken) public ActionResult<User> CreateUser([FromBody] User newUser, Token authToken)
{ {
return Db.GetSession(authToken).Create(newUser) return Db.GetSession(authToken).Create(newUser)
? newUser.HidePassword() ? newUser.HidePassword()
@ -260,7 +263,7 @@ public class Controller : ControllerBase
} }
[HttpPost(nameof(CreateInstallation))] [HttpPost(nameof(CreateInstallation))]
public async Task<ActionResult<Installation>> CreateInstallation([FromBody]Installation installation, Token authToken) public async Task<ActionResult<Installation>> CreateInstallation([FromBody] Installation installation, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
@ -271,7 +274,7 @@ public class Controller : ControllerBase
} }
[HttpPost(nameof(CreateFolder))] [HttpPost(nameof(CreateFolder))]
public ActionResult<Folder> CreateFolder(Folder folder, Token authToken) public ActionResult<Folder> CreateFolder([FromBody] Folder folder, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
@ -331,7 +334,7 @@ public class Controller : ControllerBase
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
// TODO: automatic BadRequest when properties are null during deserialization // TODO: automatic BadRequest when properties are null during deserialization
var installation = Db.GetFolderById(installationAccess.InstallationId); var installation = Db.GetInstallationById(installationAccess.InstallationId);
var user = Db.GetUserById(installationAccess.UserId); var user = Db.GetUserById(installationAccess.UserId);
return session.RevokeUserAccessTo(user, installation) return session.RevokeUserAccessTo(user, installation)
@ -342,7 +345,7 @@ public class Controller : ControllerBase
[HttpPut(nameof(UpdateUser))] [HttpPut(nameof(UpdateUser))]
public ActionResult<User> UpdateUser(User updatedUser, Token authToken) public ActionResult<User> UpdateUser([FromBody] User updatedUser, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
@ -366,7 +369,7 @@ public class Controller : ControllerBase
[HttpPut(nameof(UpdateInstallation))] [HttpPut(nameof(UpdateInstallation))]
public ActionResult<Installation> UpdateInstallation(Installation installation, Token authToken) public ActionResult<Installation> UpdateInstallation([FromBody] Installation installation, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);
@ -378,7 +381,7 @@ public class Controller : ControllerBase
[HttpPut(nameof(UpdateFolder))] [HttpPut(nameof(UpdateFolder))]
public ActionResult<Folder> UpdateFolder(Folder folder, Token authToken) public ActionResult<Folder> UpdateFolder([FromBody] Folder folder, Token authToken)
{ {
var session = Db.GetSession(authToken); var session = Db.GetSession(authToken);

View File

@ -9,8 +9,9 @@ public class Installation : TreeNode
public String Country { get; set; } = ""; public String Country { get; set; } = "";
// TODO: make relation // TODO: make relation
[Ignore] public IReadOnlyList<String> OrderNumbers { get; set; } = Array.Empty<String>(); //public IReadOnlyList<String> OrderNumbers { get; set; } = Array.Empty<String>();
public String OrderNumbers { get; set; } = "";
public Double Lat { get; set; } public Double Lat { get; set; }
public Double Long { get; set; } public Double Long { get; set; }

View File

@ -52,6 +52,11 @@ public static class FolderMethods
.Skip(1); // skip self .Skip(1); // skip self
} }
public static IEnumerable<Folder> DescendantFoldersAndSelf(this Folder parent)
{
return parent
.TraverseDepthFirstPreOrder(ChildFolders);
}
public static Boolean IsDescendantOf(this Folder folder, Folder ancestor) public static Boolean IsDescendantOf(this Folder folder, Folder ancestor)
{ {
return folder return folder

View File

@ -1,4 +1,5 @@
using InnovEnergy.App.Backend.Database; using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.App.Backend.S3; using InnovEnergy.App.Backend.S3;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
@ -119,12 +120,28 @@ public static class InstallationMethods
return Db.Installations.Any(i => i.Id == installation.Id); return Db.Installations.Any(i => i.Id == installation.Id);
} }
public static IReadOnlyList<String> GetOrderNumbers(this Installation installation) public static Boolean SetOrderNumbers(this Installation installation)
{ {
return Db.OrderNumber2Installation foreach (var orderNumber in installation.OrderNumbers.Split(','))
{
var o2I = new OrderNumber2Installation
{
OrderNumber = orderNumber,
InstallationId = installation.Id
};
Db.Create(o2I);
}
return true;
}
public static String GetOrderNumbers(this Installation installation)
{
return string.Join(", ", Db.OrderNumber2Installation
.Where(i => i.InstallationId == installation.Id) .Where(i => i.InstallationId == installation.Id)
.Select(i => i.OrderNumber) .Select(i => i.OrderNumber)
.ToReadOnlyList<String>(); .ToReadOnlyList());
} }
public static Installation FillOrderNumbers(this Installation installation) public static Installation FillOrderNumbers(this Installation installation)

View File

@ -83,18 +83,19 @@ public static class SessionMethods
var user = session?.User; var user = session?.User;
return user is not null return user is not null
&& installation is not null && installation is not null
&& user.HasWriteAccess && user.HasWriteAccess
&& user.HasAccessToParentOf(installation) && user.HasAccessToParentOf(installation)
&& Db.Create(installation) // TODO: these two in a transaction && Db.Create(installation) // TODO: these two in a transaction
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id }) && installation.SetOrderNumbers()
&& await installation.CreateBucket() && Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id });
&& await installation.RenewS3Credentials(); // generation of access _after_ generation of //&& await installation.CreateBucket()
// bucket to prevent "zombie" access-rights. //&& await installation.RenewS3Credentials(); // generation of access _after_ generation of
// This might fuck us over if the creation of access rights fails, // bucket to prevent "zombie" access-rights.
// as bucket-names are unique and bound to the installation id... -K // This might fuck us over if the creation of access rights fails,
// as bucket-names are unique and bound to the installation id... -K
} }
public static Boolean Update(this Session? session, Installation? installation) public static Boolean Update(this Session? session, Installation? installation)
{ {
var user = session?.User; var user = session?.User;
@ -104,7 +105,7 @@ public static class SessionMethods
if (!Equals(originalOrderNumbers, installation?.OrderNumbers)) if (!Equals(originalOrderNumbers, installation?.OrderNumbers))
{ {
foreach (var orderNumber in installation!.OrderNumbers) foreach (var orderNumber in installation!.OrderNumbers.Split(','))
{ {
if (originalOrderNumbers.Contains(orderNumber)) continue; if (originalOrderNumbers.Contains(orderNumber)) continue;
var o2I = new OrderNumber2Installation var o2I = new OrderNumber2Installation
@ -115,7 +116,7 @@ public static class SessionMethods
Db.Create(o2I); Db.Create(o2I);
} }
foreach (var orderNumberOld in originalOrderNumbers) foreach (var orderNumberOld in originalOrderNumbers.Split(','))
{ {
if (!installation!.OrderNumbers.Contains(orderNumberOld)) if (!installation!.OrderNumbers.Contains(orderNumberOld))
{ {
@ -138,13 +139,13 @@ public static class SessionMethods
public static async Task<Boolean> Delete(this Session? session, Installation? installation) public static async Task<Boolean> Delete(this Session? session, Installation? installation)
{ {
var user = session?.User; var user = session?.User;
return user is not null return user is not null
&& installation is not null && installation is not null
&& user.HasWriteAccess && user.HasWriteAccess
&& user.HasAccessTo(installation) && user.HasAccessTo(installation)
&& Db.Delete(installation) && Db.Delete(installation);
&& await installation.DeleteBucket(); //&& await installation.DeleteBucket();
} }
public static Boolean Create(this Session? session, User newUser) public static Boolean Create(this Session? session, User newUser)
@ -158,7 +159,7 @@ public static class SessionMethods
&& newUser && newUser
.WithParent(sessionUser) .WithParent(sessionUser)
.Do(() => newUser.MustResetPassword = true) .Do(() => newUser.MustResetPassword = true)
.Do(() => newUser.Password = newUser.SaltAndHashPassword(newUser.Password)) .Do(() => newUser.Password = null)
.Apply(Db.Create); .Apply(Db.Create);
// && Mailer.Mailer.SendVerificationMessage(newUser); // && Mailer.Mailer.SendVerificationMessage(newUser);

View File

@ -5,7 +5,7 @@ namespace InnovEnergy.App.Backend.DataTypes;
public abstract partial class TreeNode public abstract partial class TreeNode
{ {
[PrimaryKey, AutoIncrement] [PrimaryKey, AutoIncrement]
public virtual Int64 Id { get; set; } public virtual Int64 Id { get; set; }
public virtual String Name { get; set; } = ""; // overridden by User (unique) public virtual String Name { get; set; } = ""; // overridden by User (unique)
public String Information { get; set; } = ""; // unstructured random info public String Information { get; set; } = ""; // unstructured random info

View File

@ -8,7 +8,7 @@ public class User : TreeNode
public Boolean HasWriteAccess { get; set; } = false; public Boolean HasWriteAccess { get; set; } = false;
public Boolean MustResetPassword { get; set; } = false; public Boolean MustResetPassword { get; set; } = false;
public String Language { get; set; } = null!; public String Language { get; set; } = null!;
public String Password { get; set; } = null!; public String? Password { get; set; } = null!;
[Unique] [Unique]
public override String Name { get; set; } = null!; public override String Name { get; set; } = null!;

View File

@ -27,7 +27,8 @@ public static partial class Db
.Last().Name; .Last().Name;
var fileConnection = new SQLiteConnection("DbBackups/"+latestDb); var fileConnection = new SQLiteConnection("DbBackups/"+latestDb);
Console.Out.Write(latestDb);
var memoryConnection = new SQLiteConnection(":memory:"); var memoryConnection = new SQLiteConnection(":memory:");
// fileConnection.Backup(memoryConnection.DatabasePath); // fileConnection.Backup(memoryConnection.DatabasePath);
@ -74,7 +75,7 @@ public static partial class Db
public static void BackupDatabase() public static void BackupDatabase()
{ {
var filename = "db-" + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ".sqlite"; var filename = "db-" + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ".sqlite";
Connection.Backup("DbBackups/"+filename); Connection.Backup("DbBackups/" + filename);
} }
public static TableQuery<Session> Sessions => Connection.Table<Session>(); public static TableQuery<Session> Sessions => Connection.Table<Session>();

View File

@ -10,12 +10,18 @@ public static partial class Db
{ {
public static Boolean Delete(Folder folder) public static Boolean Delete(Folder folder)
{ {
return RunTransaction(DeleteFolderAndAllItsDependencies); var deleteSuccess= RunTransaction(DeleteFolderAndAllItsDependencies);
if (deleteSuccess)
{
BackupDatabase();
}
return deleteSuccess;
Boolean DeleteFolderAndAllItsDependencies() Boolean DeleteFolderAndAllItsDependencies()
{ {
return folder return folder
.DescendantFolders() .DescendantFoldersAndSelf()
.All(DeleteDescendantFolderAndItsDependencies); .All(DeleteDescendantFolderAndItsDependencies);
} }
@ -24,10 +30,8 @@ public static partial class Db
FolderAccess .Delete(r => r.FolderId == f.Id); FolderAccess .Delete(r => r.FolderId == f.Id);
Installations.Delete(r => r.ParentId == f.Id); Installations.Delete(r => r.ParentId == f.Id);
var delete = Folders.Delete(r => r.Id == f.Id); var delete = Folders.Delete(r => r.Id == f.Id);
var deleteSuccess = delete > 0;
if (deleteSuccess) return delete>0;
BackupDatabase();
return deleteSuccess;
} }
} }
@ -42,6 +46,7 @@ public static partial class Db
Boolean DeleteInstallationAndItsDependencies() Boolean DeleteInstallationAndItsDependencies()
{ {
InstallationAccess.Delete(i => i.InstallationId == installation.Id); InstallationAccess.Delete(i => i.InstallationId == installation.Id);
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
return Installations.Delete(i => i.Id == installation.Id) > 0; return Installations.Delete(i => i.Id == installation.Id) > 0;
} }
} }

View File

@ -3,6 +3,8 @@ using InnovEnergy.App.Backend.Database;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi.Models;
using System.Net;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend; namespace InnovEnergy.App.Backend;
@ -12,8 +14,8 @@ public static class Program
{ {
//Db.CreateFakeRelations(); //Db.CreateFakeRelations();
Db.Init(); Db.Init();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddProblemDetails(setup => builder.Services.AddProblemDetails(setup =>
{ {
@ -21,7 +23,7 @@ public static class Program
setup.IncludeExceptionDetails = (ctx, env) => builder.Environment.IsDevelopment() || builder.Environment.IsStaging(); setup.IncludeExceptionDetails = (ctx, env) => builder.Environment.IsDevelopment() || builder.Environment.IsStaging();
//This handles our Exceptions //This handles our Exceptions
setup.Map<Exceptions>(exception => new ProblemDetails() setup.Map<Exceptions>(exception => new ProblemDetails
{ {
Detail = exception.Detail, Detail = exception.Detail,
Status = exception.Status, Status = exception.Status,
@ -38,6 +40,16 @@ public static class Program
}); });
var app = builder.Build(); var app = builder.Build();
app.Use(async (context, next) =>
{
var x = 2;
context.Request.WriteLine();
await next(context);
});
app.UseForwardedHeaders(new ForwardedHeadersOptions app.UseForwardedHeaders(new ForwardedHeadersOptions
{ {
@ -51,12 +63,11 @@ public static class Program
} }
app.UseCors(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()) ; app.UseCors(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()) ;
app.UseHttpsRedirection(); //app.UseHttpsRedirection();
app.MapControllers(); app.MapControllers();
app.UseProblemDetails(); app.UseProblemDetails();
app.Run();
app.Run();
} }
private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo

View File

@ -3,7 +3,7 @@ from flask import Flask
from json2html import json2html from json2html import json2html
app = Flask(__name__) app = Flask(__name__)
serverUrl = "https://127.0.0.1:7087/api" #todo change me serverUrl = "https://127.0.0.1:8000/api" #todo change me
@app.route('/') @app.route('/')
def hello(): def hello():

View File

@ -35,6 +35,7 @@
"yup": "^1.1.0" "yup": "^1.1.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",

View File

@ -1,11 +1,11 @@
import useToken from "./hooks/useToken"; import useToken from "./hooks/useToken";
import Login from "./Login"; import Login from "./Login";
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; import {BrowserRouter, Navigate, Route, Routes} from "react-router-dom";
import { Container, Grid, Box } from "@mui/material"; import {Box, Container, Grid} from "@mui/material";
import routes from "./routes.json"; import routes from "./routes.json";
import { IntlProvider } from "react-intl"; import {IntlProvider} from "react-intl";
import { useContext, useState } from "react"; import {useContext, useState} from "react";
import en from "./lang/en.json"; import en from "./lang/en.json";
import de from "./lang/de.json"; import de from "./lang/de.json";
import fr from "./lang/fr.json"; import fr from "./lang/fr.json";
@ -14,10 +14,10 @@ import LogoutButton from "./components/Layout/LogoutButton";
import Users from "./components/Users/Users"; import Users from "./components/Users/Users";
import NavigationTabs from "./components/Layout/NavigationTabs"; import NavigationTabs from "./components/Layout/NavigationTabs";
import InstallationPage from "./components/Installations/InstallationPage"; import InstallationPage from "./components/Installations/InstallationPage";
import { UserContext } from "./components/Context/UserContextProvider"; import {UserContext} from "./components/Context/UserContextProvider";
import ResetPassword from "./ResetPassword"; import ResetPassword from "./ResetPassword";
import innovenergyLogo from "./resources/innoveng_logo_on_orange.png"; import innovenergyLogo from "./resources/innoveng_logo_on_orange.png";
import { colors } from "./index"; import {colors} from "./index";
const App = () => { const App = () => {
const { token, setToken, removeToken } = useToken(); const { token, setToken, removeToken } = useToken();
@ -51,8 +51,10 @@ const App = () => {
> >
<Container maxWidth="xl" sx={{ pt: 2 }}> <Container maxWidth="xl" sx={{ pt: 2 }}>
<Grid container> <Grid container>
<Grid item xs={3} container justifyContent="flex-start" mb={2}> <Grid item xs={3} container justifyContent="flex-start" mb={2} >
<img src={innovenergyLogo} alt="innovenergy logo" height="100" /> <a href="https://www.innov.energy/de/" >
<img src={innovenergyLogo} alt="innovenergy logo" height="100" />
</a>
</Grid> </Grid>
<Grid <Grid
item item

View File

@ -11,7 +11,7 @@ interface I_S3CredentialsContextProviderProps {
saveS3Credentials: (credentials: I_S3Credentials, id: string) => void; saveS3Credentials: (credentials: I_S3Credentials, id: string) => void;
fetchData: (timestamp: UnixTime) => Promise<FetchResult<DataRecord>>; fetchData: (timestamp: UnixTime) => Promise<FetchResult<DataRecord>>;
} }
export const S3CredentialsContext = export const S3CredentialsContext =
createContext<I_S3CredentialsContextProviderProps>({ createContext<I_S3CredentialsContextProviderProps>({
s3Credentials: {} as I_S3Credentials, s3Credentials: {} as I_S3Credentials,
@ -27,7 +27,7 @@ const S3CredentialsContextProvider = ({
const [s3Credentials, setS3Credentials] = useState<I_S3Credentials>(); const [s3Credentials, setS3Credentials] = useState<I_S3Credentials>();
const saveS3Credentials = (credentials: I_S3Credentials, id: string) => { const saveS3Credentials = (credentials: I_S3Credentials, id: string) => {
const s3Bucket = id + "-3e5b3069-214a-43ee-8d85-57d72000c10d"; const s3Bucket = id + "-3e5b3069-214a-43ee-8d85-57d72000c19d";
setS3Credentials({ s3Bucket, ...credentials }); setS3Credentials({ s3Bucket, ...credentials });
}; };

View File

@ -1,6 +1,7 @@
import { createContext, ReactNode, useState } from "react"; import { createContext, ReactNode, useState } from "react";
import { I_User } from "../../util/user.util"; import { I_User } from "../../util/user.util";
interface I_InstallationContextProviderProps { interface I_InstallationContextProviderProps {
currentUser?: I_User; currentUser?: I_User;
setCurrentUser: (value: I_User) => void; setCurrentUser: (value: I_User) => void;

View File

@ -33,7 +33,7 @@ const InstallationForm = (props: I_InstallationFormProps) => {
const readOnly = !getCurrentUser().hasWriteAccess; const readOnly = !getCurrentUser().hasWriteAccess;
const intl = useIntl(); const intl = useIntl();
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
name: Yup.string().required( name: Yup.string().required(
intl.formatMessage({ intl.formatMessage({
@ -53,6 +53,19 @@ const InstallationForm = (props: I_InstallationFormProps) => {
defaultMessage: "Location is required", defaultMessage: "Location is required",
}) })
), ),
country: Yup.string().required(
intl.formatMessage({
id: "requiredCountry",
defaultMessage: "Country is required",
})
),
orderNumbers: Yup.string().required(
intl.formatMessage({
id: "requiredOrderNumber",
defaultMessage: "Order Number is required",
})
),
}); });
const formik = useFormik({ const formik = useFormik({
@ -64,6 +77,12 @@ const InstallationForm = (props: I_InstallationFormProps) => {
orderNumbers: values.orderNumbers, orderNumbers: values.orderNumbers,
}, },
onSubmit: (formikValues) => { onSubmit: (formikValues) => {
/*const updatedValues = {
...formikValues,
orderNumbers: formikValues.orderNumbers.split(','),
};*/
handleSubmit(values, formikValues) handleSubmit(values, formikValues)
.then(() => { .then(() => {
setOpen(true); setOpen(true);
@ -164,12 +183,22 @@ const InstallationForm = (props: I_InstallationFormProps) => {
additionalButtons.map((button) => button)} additionalButtons.map((button) => button)}
{!readOnly && ( {!readOnly && (
<InnovenergyButton id="installation-form-submit-button" type="submit"> <InnovenergyButton id="installation-form-submit-button" type="submit">
<FormattedMessage <FormattedMessage
id="applyChanges" id="applyChanges"
defaultMessage="Apply changes" defaultMessage="Apply changes"
/> />
</InnovenergyButton> </InnovenergyButton>
)} )}
{!readOnly && (
<InnovenergyButton id="installation-form-submit-button" type="submit" sx={{ marginLeft: 1}}>
<FormattedMessage
id="deleteInstallation"
defaultMessage="Delete installation"
/>
</InnovenergyButton>
)}
</Grid> </Grid>
<Snackbar <Snackbar
open={open} open={open}

View File

@ -66,4 +66,4 @@ const Installations = () => {
); );
}; };
export default Installations; export default Installations;

View File

@ -16,12 +16,7 @@ const NavigationTabs = () => {
return ( return (
<> <>
<InnovenergyTabs <InnovenergyTabs
sx={{
paddingTop: 0,
"&.MuiTabs-root": {
borderBottom: "none",
},
}}
id="navigation-buttons-tabs" id="navigation-buttons-tabs"
value={routeMatch?.pattern?.path} value={routeMatch?.pattern?.path}
> >

View File

@ -2,6 +2,7 @@
"liveView": "Live view", "liveView": "Live view",
"allInstallations": "All installations", "allInstallations": "All installations",
"applyChanges": "Apply changes", "applyChanges": "Apply changes",
"deleteInstallation": "Delete Installation",
"country": "Country", "country": "Country",
"customerName": "Customer name", "customerName": "Customer name",
"english": "English", "english": "English",
@ -44,6 +45,7 @@
"requiredLocation": "Location is required", "requiredLocation": "Location is required",
"requiredName": "Name is required", "requiredName": "Name is required",
"requiredRegion": "Region is required", "requiredRegion": "Region is required",
"requiredOrderNumber": "Required Order Number",
"submit": "Submit", "submit": "Submit",
"user": "User", "user": "User",
"userTabs": "user tabs" "userTabs": "user tabs"

View File

@ -0,0 +1,26 @@
import axios from 'axios';
export const axiosConfigWithoutToken = axios.create({
baseURL: 'https://localhost:7087/api'
});
const axiosConfig = axios.create({
baseURL: 'https://localhost:7087/api'
});
axiosConfig.defaults.params = {};
axiosConfig.interceptors.request.use(
(config) => {
const tokenString = localStorage.getItem('token');
const token = tokenString !== null ? tokenString : '';
if (token) {
config.params['authToken'] = token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default axiosConfig;

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,126 @@
import React, { useState } from 'react';
import {
Card,
Divider,
Grid,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useTheme
} from '@mui/material';
import { InnovEnergyUser } from 'src/interfaces/UserTypes';
import User from './User';
interface FlatUsersViewProps {
users: InnovEnergyUser[];
fetchDataAgain: () => void;
}
const FlatUsersView = (props: FlatUsersViewProps) => {
const [selectedUser, setSelectedUser] = useState<number>(-1);
const selectedBulkActions = selectedUser !== -1;
const handleSelectOneUser = (installationID: number): void => {
if (selectedUser != installationID) {
setSelectedUser(installationID);
} else {
setSelectedUser(-1);
}
};
const theme = useTheme();
const [isRowHovered, setHoveredRow] = useState(-1);
const handleRowMouseEnter = (id: number) => {
setHoveredRow(id);
};
const handleRowMouseLeave = () => {
setHoveredRow(-1);
};
const findUser = (id: number) => {
return props.users.find((user) => user.id === id);
};
return (
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
<Grid item xs={12} md={3}>
<Card>
<Divider />
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell padding="checkbox"></TableCell>
<TableCell>Username</TableCell>
<TableCell>Email</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.users.map((user) => {
const isInstallationSelected = user.id === selectedUser;
const rowStyles =
isRowHovered === user.id
? {
cursor: 'pointer',
backgroundColor: theme.colors.primary.lighter // Set your desired hover background color here
}
: {};
return (
<TableRow
hover
key={user.id}
selected={isInstallationSelected}
onClick={() => handleSelectOneUser(user.id)}
style={rowStyles}
onMouseEnter={() => handleRowMouseEnter(user.id)}
onMouseLeave={() => handleRowMouseLeave()}
>
<TableCell padding="checkbox"></TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{user.name}
</Typography>
</TableCell>
<TableCell>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{user.email}
</Typography>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</Card>
</Grid>
{selectedBulkActions && (
<User
current_user={findUser(selectedUser)}
fetchDataAgain={props.fetchDataAgain}
></User>
)}
</Grid>
);
};
export default FlatUsersView;

View File

@ -0,0 +1,90 @@
import React, { useContext, useEffect, useState } from 'react';
import {
FormControl,
Grid,
InputAdornment,
TextField,
useTheme
} from '@mui/material';
import SearchTwoToneIcon from '@mui/icons-material/SearchTwoTone';
import FlatUsersView from './FlatUsersView';
import { UsersContext } from '../../../contexts/UsersContextProvider';
import Button from '@mui/material/Button';
import UserForm from './userForm';
import { UserContext } from '../../../contexts/userContext';
function UsersSearch() {
const theme = useTheme();
const [searchTerm, setSearchTerm] = useState('');
const { availableUsers, fetchAvailableUsers } = useContext(UsersContext);
const [filteredData, setFilteredData] = useState(availableUsers);
const [openModal, setOpenModal] = useState(false);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
useEffect(() => {
fetchAvailableUsers();
}, []);
const fetchDataAgain = () => {
fetchAvailableUsers();
};
useEffect(() => {
const filtered = availableUsers.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredData(filtered);
}, [searchTerm, availableUsers]);
const handleSubmit = () => {
setOpenModal(true);
};
const handleUserFormSubmit = () => {
setOpenModal(false);
fetchAvailableUsers();
};
const handleUserFormCancel = () => {
setOpenModal(false);
};
return (
<>
<Grid container spacing={1}>
<Grid item xs={12} md={3}>
{currentUser.hasWriteAccess && (
<Button variant="contained" onClick={handleSubmit}>
Create user
</Button>
)}
</Grid>
</Grid>
{openModal && (
<UserForm cancel={handleUserFormCancel} submit={handleUserFormSubmit} />
)}
<Grid container spacing={1} sx={{ marginTop: '1px' }}>
<Grid item xs={12} md={3}>
<FormControl variant="outlined" fullWidth>
<TextField
placeholder="Search"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchTwoToneIcon />
</InputAdornment>
)
}}
/>
</FormControl>
</Grid>
</Grid>
<FlatUsersView users={filteredData} fetchDataAgain={fetchDataAgain} />
</>
);
}
export default UsersSearch;

View File

@ -0,0 +1,25 @@
import Footer from 'src/components/Footer';
import { Box, Container, Grid, useTheme } from '@mui/material';
import UsersSearch from './UsersSearch';
import UsersContextProvider from 'src/contexts/UsersContextProvider';
function Users() {
const theme = useTheme();
return (
<>
<UsersContextProvider>
<Container maxWidth="xl" sx={{ marginTop: '20px' }}>
<Grid item xs={12}>
<Box p={4}>
<UsersSearch />
</Box>
</Grid>
</Container>
<Footer />
</UsersContextProvider>
</>
);
}
export default Users;

View File

@ -0,0 +1,47 @@
{
"information": "Information",
"addNewChild": "Neues Kind hinzufügen",
"addNewDialogButton": "Neue Dialogschaltfläche hinzufügen",
"addUser": "Nutzer erstellen",
"alarms": "Alarme",
"applyChanges": "Änderungen speichern",
"country": "Land",
"createNewFolder": "Neuen Ordner erstellen",
"createNewUser": "Neuen Nutzer erstellen",
"customerName": "Kundenname",
"email": "Email",
"english": "Englisch",
"error": "Fehler",
"folder": "Ordner",
"german": "Deutsch",
"groupTabs": "Gruppen",
"groupTree": "Gruppenbaum",
"information": "Information",
"inheritedAccess": "Vererbter Zugriff von",
"installation": "Installation",
"installationTabs": "Installationen",
"installations": "Installationen",
"lastWeek": "Letzte Woche",
"location": "Standort",
"log": "Logbuch",
"logout": "Abmelden",
"makeASelection": "Bitte wählen Sie links eine Auswahl",
"manageAccess": "Zugriff verwalten",
"move": "Verschieben",
"moveTo": "Verschieben zu",
"moveTree": "Baum verschieben",
"name": "Name",
"navigationTabs": "Navigation",
"orderNumbers": "Bestellnummer",
"region": "Region",
"requiredLocation": "Standort ist erforderlich",
"requiredName": "Name ist erforderlich",
"requiredRegion": "Region ist erforderlich",
"search": "Suche",
"submit": "Senden",
"updateFolderErrorMessage": "Fehler, Ordner kann nicht aktualisiert werden",
"updatedSuccessfully": "Erfolgreich aktualisiert",
"user": "Nutzer",
"userTabs": "Nutzer",
"users": "Nutzer"
}

View File

@ -0,0 +1,52 @@
{
"liveView": "Live view",
"allInstallations": "All installations",
"applyChanges": "Apply changes",
"deleteInstallation": "Delete Installation",
"country": "Country",
"customerName": "Customer name",
"english": "English",
"german": "German",
"installation": "Installation",
"location": "Location",
"log": "Log",
"orderNumbers": "Order numbers",
"region": "Region",
"search": "Search",
"users": "Users",
"logout": "Logout",
"updatedSuccessfully": "Updated successfully",
"groups": "Groups",
"group": "Group",
"folder": "Folder",
"updateFolderErrorMessage": "Couldn't update folder, an error occured",
"Information": "Information",
"addNewChild": "Add new child",
"addNewDialogButton": "Add new dialog button",
"addUser": "Create user",
"createNewFolder": "Create new folder",
"createNewUser": "Create new user",
"email": "Email",
"error": "",
"groupTabs": "Group tabs",
"groupTree": "Group tree",
"information": "Information",
"inheritedAccess": "Inherited access from",
"installationTabs": "Installation tabs",
"installations": "Installations",
"lastWeek": "Last week",
"makeASelection": "Please make a selection on the left",
"manageAccess": "Manage access",
"move": "Move",
"moveTo": "Move to",
"moveTree": "Move tree",
"name": "Name",
"navigationTabs": "Navigation tabs",
"requiredLocation": "Location is required",
"requiredName": "Name is required",
"requiredRegion": "Region is required",
"requiredOrderNumber": "Required Order Number",
"submit": "Submit",
"user": "User",
"userTabs": "user tabs"
}

View File

@ -0,0 +1,47 @@
{
"information": "Informations",
"addNewChild": "Ajouter un nouvel enfant",
"addNewDialogButton": "Ajouter un nouveau bouton de dialogue",
"addUser": "Créer un utilisateur",
"alarms": "Alarmes",
"applyChanges": "Appliquer les modifications",
"country": "Pays",
"createNewFolder": "Créer un nouveau dossier",
"createNewUser": "Créer un nouvel utilisateur",
"customerName": "Nom du client",
"email": "E-mail",
"english": "Anglais",
"error": "Erreur",
"folder": "Dossier",
"german": "Allemand",
"groupTabs": "Onglets de groupe",
"groupTree": "Arbre de groupe",
"information": "Informations",
"inheritedAccess": "Accès hérité de",
"installation": "Installation",
"installationTabs": "Onglets d'installation",
"installations": "Installations",
"lastWeek": "La semaine dernière",
"location": "Localisation",
"log": "Journal",
"logout": "Déconnexion",
"makeASelection": "Veuillez faire une sélection à gauche",
"manageAccess": "Gérer l'accès",
"move": "Déplacer",
"moveTo": "Déplacer à",
"moveTree": "Déplacer l'arbre",
"name": "Nom",
"navigationTabs": "Onglets de navigation",
"orderNumbers": "Numéro de commande",
"region": "Région",
"requiredLocation": "L'emplacement est requis",
"requiredName": "Le nom est obligatoire",
"requiredRegion": "La région est obligatoire",
"search": "Recherche",
"submit": "Soumettre",
"updateFolderErrorMessage": "Une erreur s'est produite, impossible de mettre à jour le dossier.",
"updatedSuccessfully": "Mise à jour réussie",
"user": "Utilisateur",
"userTabs": "Onglets utilisateurs",
"users": "Utilisateurs"
}