+ Added migration

+ Added STATIC data into Database (Roles functions and users)
+ Configuration controller and startupConfig API
* Refactor api names
This commit is contained in:
Lucio Maranta
2018-01-10 17:21:40 +01:00
parent 61869c6fc1
commit 796801f7ee
15 changed files with 380 additions and 69 deletions
+14 -5
View File
@@ -23,9 +23,18 @@ namespace Step.Database.Controllers
}
public void Create(string username, string password, string firstName, string lastName, int roleId, CultureInfo language)
{
UserModel user = CreateUserModel(username, password, firstName, lastName, roleId, language);
// Add to database
dbCtx.Users.Add(user);
// Commit changes
dbCtx.SaveChanges();
}
public static UserModel CreateUserModel(int id, string username, string password, string firstName, string lastName, int roleId, CultureInfo language)
{
// Create a new user model with params
UserModel user = new UserModel()
return new UserModel()
{
Username = username,
Password = Crypto.HashPassword(password),
@@ -35,10 +44,10 @@ namespace Step.Database.Controllers
SecurityStamp = Guid.NewGuid().ToString(),
Language = language
};
// Add to database
dbCtx.Users.Add(user);
// Commit changes
dbCtx.SaveChanges();
}
public static UserModel CreateUserModel(string username, string password, string firstName, string lastName, int roleId, CultureInfo language)
{
return CreateUserModel(0, username, password, firstName, lastName, roleId, language);
}
public UserModel Find(int id)
+4 -5
View File
@@ -5,6 +5,7 @@ using Step.Model.DatabaseModels;
using MySql.Data.Entity;
using static Step.Utils.ExceptionManager;
using static Step.Utils.Constants;
using Step.Database.Migrations;
namespace Step.Database
{
@@ -22,6 +23,8 @@ namespace Step.Database
public static void TestDatabaseConnection()
{
System.Data.Entity.Database.SetInitializer(new MigrateDatabaseToLatestVersion<DatabaseContext, Configuration>());
using (DatabaseContext dbContext = new DatabaseContext())
{
try
@@ -33,7 +36,7 @@ namespace Step.Database
{
if (ex.Number == 0)
{
dbContext.Database.CreateIfNotExists();
dbContext.Database.Initialize(true);
}
else if (ex.Number == 1042) // Can't find MySQLServer
{
@@ -46,9 +49,5 @@ namespace Step.Database
}
}
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
System.Data.Entity.Database.SetInitializer<DatabaseContext>(null);
}
}
}
@@ -0,0 +1,29 @@
// <auto-generated />
namespace Step.Database.Migrations
{
using System.CodeDom.Compiler;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.Resources;
[GeneratedCode("EntityFramework.Migrations", "6.2.0-61023")]
public sealed partial class InizialCreate : IMigrationMetadata
{
private readonly ResourceManager Resources = new ResourceManager(typeof(InizialCreate));
string IMigrationMetadata.Id
{
get { return "201801101327160_InizialCreate"; }
}
string IMigrationMetadata.Source
{
get { return null; }
}
string IMigrationMetadata.Target
{
get { return Resources.GetString("Target"); }
}
}
}
@@ -0,0 +1,61 @@
namespace Step.Database.Migrations
{
using System;
using System.Data.Entity.Migrations;
public partial class InizialCreate : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.functions_access",
c => new
{
id = c.Int(nullable: false, identity: true),
name = c.String(unicode: false),
write_level_min = c.Int(nullable: false),
read_level_min = c.Int(nullable: false),
area = c.String(unicode: false),
enabled = c.Boolean(nullable: false),
})
.PrimaryKey(t => t.id);
CreateTable(
"dbo.roles",
c => new
{
id = c.Int(nullable: false, identity: true),
name = c.String(unicode: false),
level = c.Int(nullable: false),
})
.PrimaryKey(t => t.id);
CreateTable(
"dbo.users",
c => new
{
id = c.Int(nullable: false, identity: true),
username = c.String(nullable: false, unicode: false),
first_name = c.String(unicode: false),
last_name = c.String(unicode: false),
password = c.String(unicode: false),
security_stamp = c.String(unicode: false),
language = c.String(unicode: false),
role_id = c.Int(nullable: false),
})
.PrimaryKey(t => t.id)
.ForeignKey("dbo.roles", t => t.role_id, cascadeDelete: true)
.Index(t => t.role_id);
}
public override void Down()
{
DropForeignKey("dbo.users", "role_id", "dbo.roles");
DropIndex("dbo.users", new[] { "role_id" });
DropTable("dbo.users");
DropTable("dbo.roles");
DropTable("dbo.functions_access");
}
}
}
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="Target" xml:space="preserve">
<value>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</value>
</data>
<data name="DefaultSchema" xml:space="preserve">
<value>dbo</value>
</data>
</root>
+37
View File
@@ -0,0 +1,37 @@
namespace Step.Database.Migrations
{
using Step.Database.Controllers;
using Step.Model.DatabaseModels;
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Globalization;
using System.Linq;
public sealed class Configuration : DbMigrationsConfiguration<Step.Database.DatabaseContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
}
protected override void Seed(DatabaseContext context)
{
// This method will be called after migrating to the latest version.
context.Roles.AddOrUpdate(
new RoleModel() { RoleId = 1, Level = 10, Name = "Admin" },
new RoleModel() { RoleId = 2, Level = 1, Name = "Guest" }
);
context.FunctionsAccess.AddOrUpdate(
new FunctionAccessModel() { FunctionAccessId = 1, Name = "test", Area = "production", Enabled = true, WriteLevelMin = 7, ReadLevelMin = 1 },
new FunctionAccessModel() { FunctionAccessId = 2, Name = "ncData", Area = "production", Enabled = true, WriteLevelMin = 100, ReadLevelMin = 1 },
new FunctionAccessModel() { FunctionAccessId = 3, Name = "functionAccess", Area = "production", Enabled = true, WriteLevelMin = 100, ReadLevelMin = 1 }
);
context.Users.AddOrUpdate
(
UsersController.CreateUserModel("cms", "cms", "cms", "cms", 1, new CultureInfo("en"))
);
}
}
}
+10
View File
@@ -70,6 +70,11 @@
<Compile Include="Controllers\FunctionAccessController.cs" />
<Compile Include="Controllers\UsersController.cs" />
<Compile Include="DatabaseContext.cs" />
<Compile Include="Migrations\201801101327160_InizialCreate.cs" />
<Compile Include="Migrations\201801101327160_InizialCreate.Designer.cs">
<DependentUpon>201801101327160_InizialCreate.cs</DependentUpon>
</Compile>
<Compile Include="Migrations\Configuration.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
@@ -102,5 +107,10 @@
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Migrations\201801101327160_InizialCreate.resx">
<DependentUpon>201801101327160_InizialCreate.cs</DependentUpon>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -0,0 +1,20 @@
using Step.Model.ConfigModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Step.Model.DTOModels
{
public class DTOStartupConfigurationModel
{
public AreasConfigModel ProductionConfig;
public AreasConfigModel ToolingConfig;
public AreasConfigModel ReportConfig;
public AreasConfigModel AlarmsConfig;
public AreasConfigModel MaintenanceConfig;
public AreasConfigModel UtilitiesConfig;
public AreasConfigModel ScadaConfig;
}
}
+1
View File
@@ -70,6 +70,7 @@
<Compile Include="DTOModels\DTOAxesModel.cs" />
<Compile Include="DTOModels\DTOLanguageModel.cs" />
<Compile Include="DTOModels\DTONcGenericDataModel.cs" />
<Compile Include="DTOModels\DTOStartupConfigurationModel.cs" />
<Compile Include="DTOModels\DTOUserModel.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="DatabaseModels\UserModel.cs">
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Web.Http;
using Step.Database.Controllers;
using Step.Model.DTOModels;
using static Step.Utils.Constants;
namespace Step.Controllers.WebApi
{
[RoutePrefix("api/authorization")]
public class AuthorizationController : ApiController
{
[Route("functions"), HttpGet]
[WebApiAuthorize(FunctionAccess = "functionAccess", Action = ACTIONS.READ)]
public IHttpActionResult GetFunctionsConfig()
{
using (FunctionAccessController functionController = new FunctionAccessController())
{
var identity = User.Identity as ClaimsIdentity;
var userRoleLevel = identity.Claims.Where(c => c.Type == ROLE_LEVEL_KEY).SingleOrDefault();
List<DTOFunctionAccessModel> functionsList = functionController.GetFunctionAccess(Convert.ToInt32(userRoleLevel.Value));
if (functionsList == null)
return NotFound();
return Ok(functionsList);
}
}
}
}
@@ -1,36 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Step.Model.DTOModels;
using System.Web.Http;
using Step.Database.Controllers;
using Step.Model.DTOModels;
using static Step.Utils.Constants;
using static Step.Config.StartupConfig;
namespace Step.Controllers.WebApi
{
[RoutePrefix("api/config")]
[RoutePrefix("api/configuration")]
public class ConfigurationController : ApiController
{
[Route("functions"), HttpGet]
[WebApiAuthorize(FunctionAccess = "test", Action = ACTIONS.READ)]
public IHttpActionResult GetFunctionsConfig()
{
using (FunctionAccessController functionController = new FunctionAccessController())
[Route("base"), HttpGet]
public IHttpActionResult GetStartupConfiguration()
{
DTOStartupConfigurationModel startupConfiguration = new DTOStartupConfigurationModel()
{
var identity = User.Identity as ClaimsIdentity;
ProductionConfig = ProductionConfig,
AlarmsConfig = AlarmsConfig,
ScadaConfig = ScadaConfig,
MaintenanceConfig = MaintenanceConfig,
ReportConfig = ReportConfig,
ToolingConfig = ToolingConfig,
UtilitiesConfig = UtilitiesConfig
};
var userRoleLevel = identity.Claims.Where(c => c.Type == ROLE_LEVEL_KEY).SingleOrDefault();
List<DTOFunctionAccessModel> functionsList = functionController.GetFunctionAccess(Convert.ToInt32(userRoleLevel.Value));
if (functionsList == null)
return NotFound();
return Ok(functionsList);
}
return Ok(startupConfiguration);
}
}
}
+1 -19
View File
@@ -6,27 +6,9 @@ using Step.Model.DatabaseModels;
namespace Step.Controllers.WebApi
{
[RoutePrefix("api/login")]
[RoutePrefix("api/user")]
public class LoginController : ApiController
{
[WebApiAuthorize(FunctionAccess = "test", Action = ACTIONS.WRITE)]
[Route("test"), HttpGet]
public IHttpActionResult Test()
{
return Ok();
}
[WebApiAuthorize(FunctionAccess = "test", Action = ACTIONS.WRITE)]
[Route("crash"), HttpGet]
public IHttpActionResult Crash()
{
UsersController users = new UsersController();
return Ok(users.Find(13));
}
[Route("register"), HttpPost]
public IHttpActionResult CreateUser(UserModel model)
{
+1 -1
View File
@@ -16,7 +16,7 @@ namespace Step.Controllers.WebApi
public class NcApiController : ApiController
{
[Route("generic_data"), HttpGet]
[WebApiAuthorize(FunctionAccess = "test", Action = ACTIONS.READ)]
[WebApiAuthorize(FunctionAccess = "ncData", Action = ACTIONS.READ)]
public IHttpActionResult GetNcGenericData()
{
DTONcGenericDataModel genericData = new DTONcGenericDataModel();
+23 -14
View File
@@ -4,6 +4,7 @@ using Step.Database.Controllers;
using Step.Model.DatabaseModels;
using System.Security.Claims;
using static Step.Utils.Constants;
using System;
namespace Step.Provider
{
@@ -19,24 +20,32 @@ namespace Step.Provider
{
using (UsersController usersController = new UsersController())
{
// Check if credentials are correct
UserModel user = usersController.Find(context.UserName, context.Password);
// If not
if (user == null)
try
{
// Return 401 bad request
context.SetError("invalid_grant", "The user name or password is incorrect.");
// Check if credentials are correct
UserModel user = usersController.Find(context.UserName, context.Password);
// If not
if (user == null)
{
// Return 401 bad request
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
// Create a new Identity and insert custom claims
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(USERNAME_KEY, user.Username));
identity.AddClaim(new Claim(ROLE_LEVEL_KEY, user.Role.Level.ToString()));
// Create Token with identity data
context.Validated(identity);
await base.GrantResourceOwnerCredentials(context);
return;
}
// Create a new Identity and insert custom claims
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(USERNAME_KEY, user.Username));
identity.AddClaim(new Claim(ROLE_LEVEL_KEY, user.Role.Level.ToString()));
// Create Token with identity data
context.Validated(identity);
catch(Exception ex)
{
await base.GrantResourceOwnerCredentials(context);
return;
}
}
}
}
+1
View File
@@ -165,6 +165,7 @@
<Compile Include="Attributes\SignalRAuthorizeAttribute.cs" />
<Compile Include="Controllers\SignalR\DataHub.cs" />
<Compile Include="Controllers\SignalR\NcHub.cs" />
<Compile Include="Controllers\WebApi\AuthorizationController.cs" />
<Compile Include="Controllers\WebApi\ConfigurationController.cs" />
<Compile Include="Controllers\WebApi\LanguageController.cs" />
<Compile Include="Controllers\WebApi\LoginController.cs" />