Compare commits

...

4 Commits

10 changed files with 343 additions and 55 deletions

View File

@ -1,7 +1,9 @@
using Api.DBAccess;
using Api.Models;
using Microsoft.IdentityModel.Tokens;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
@ -11,34 +13,27 @@ namespace Api.AMQP
{
private readonly IConfiguration _configuration;
private readonly DbAccess _dbAccess;
private IConnection _conn;
private IChannel _channel;
private ConnectionFactory _factory;
private string _queue;
public AMQPPublisher(IConfiguration configuration, DbAccess dbAccess)
{
_dbAccess = dbAccess;
_configuration = configuration;
_factory = new ConnectionFactory();
_queue = "temperature-limits";
InitFactory();
}
public async Task Handle_Push_Device_Limits()
{
var factory = new ConnectionFactory();
var queue = "temperature-limits";
factory.UserName = _configuration["AMQP:username"];
factory.Password = _configuration["AMQP:password"];
factory.HostName = _configuration["AMQP:host"];
factory.Port = Convert.ToInt32(_configuration["AMQP:port"]);
// Connecting to our rabbitmq and after that it create's a channel where you can connect to a queue
var conn = await factory.CreateConnectionAsync();
Console.WriteLine("AMQPClient connected");
var channel = await conn.CreateChannelAsync();
// Here we connect to the queue through the channel that got created earlier
await channel.QueueDeclareAsync(queue: queue, durable: false, exclusive: false, autoDelete: false);
Console.WriteLine($"{queue} connected");
while (true)
{
await Connect();
// Publishes all devices limits
var devices = _dbAccess.ReadDevices();
foreach (var device in devices)
@ -49,33 +44,21 @@ namespace Api.AMQP
deviceLimit.TempLow = device.TempLow;
string message = JsonSerializer.Serialize(deviceLimit);
var body = Encoding.UTF8.GetBytes(message);
await channel.BasicPublishAsync(exchange: string.Empty, routingKey: queue, body: body);
await _channel.BasicPublishAsync(exchange: string.Empty, routingKey: _queue, body: body);
}
// Short delay before disconnecting from rabbitMQ
await Task.Delay(10000);
await Task.Delay(1000);
// Disconnecting from rabbitMQ to save resources
await channel.CloseAsync();
Console.WriteLine($"{queue} disconnected");
await conn.CloseAsync();
Console.WriteLine("AMQPClient disconnected");
await channel.DisposeAsync();
await conn.DisposeAsync();
await Dispose();
// 1 hour delay
await Task.Delay(3600000);
// Creating a new connection to rabbitMQ
conn = await factory.CreateConnectionAsync();
Console.WriteLine("AMQPClient connected");
channel = await conn.CreateChannelAsync();
// Here we connect to the queue through the channel that got created earlier
await channel.QueueDeclareAsync(queue: queue, durable: false, exclusive: false, autoDelete: false);
Console.WriteLine($"{queue} connected");
await Connect();
// Here all messages is consumed so the queue is empty
var consumer = new AsyncEventingBasicConsumer(channel);
var consumer = new AsyncEventingBasicConsumer(_channel);
consumer.ReceivedAsync += (model, ea) =>
{
Console.WriteLine("Emptying queue");
@ -84,9 +67,45 @@ namespace Api.AMQP
};
// Consumes the data in the queue
await channel.BasicConsumeAsync(queue, true, consumer);
await _channel.BasicConsumeAsync(_queue, true, consumer);
// Short delay before disconnecting from rabbitMQ
await Task.Delay(1000);
await Dispose();
}
}
// Disconnects from rabbitMQ
private async Task<bool> Dispose()
{
await _channel.CloseAsync();
await _conn.CloseAsync();
await _channel.DisposeAsync();
await _conn.DisposeAsync();
return true;
}
// Connects to rabbitMQ
private async Task<bool> Connect()
{
// Creating a new connection to rabbitMQ
_conn = await _factory.CreateConnectionAsync();
Console.WriteLine("AMQPClient connected");
_channel = await _conn.CreateChannelAsync();
// Here we connect to the queue through the channel that got created earlier
await _channel.QueueDeclareAsync(queue: _queue, durable: false, exclusive: false, autoDelete: false);
Console.WriteLine($"{_queue} connected");
return true;
}
// The info for the factory
private void InitFactory()
{
_factory.UserName = _configuration["AMQP:username"];
_factory.Password = _configuration["AMQP:password"];
_factory.HostName = _configuration["AMQP:host"];
_factory.Port = Convert.ToInt32(_configuration["AMQP:port"]);
}
}
}

View File

@ -4,6 +4,7 @@ using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
using System.Text.Json;
using System.Threading.Channels;
namespace Api.AMQPReciever
{
@ -11,34 +12,27 @@ namespace Api.AMQPReciever
{
private readonly IConfiguration _configuration;
private readonly DbAccess _dbAccess;
private IConnection _conn;
private IChannel _channel;
private ConnectionFactory _factory;
private string _queue;
public AMQPReciever(IConfiguration configuration, DbAccess dbAccess)
{
_dbAccess = dbAccess;
_configuration = configuration;
_factory = new ConnectionFactory();
_queue = "temperature-logs";
InitFactory();
}
public async Task Handle_Received_Application_Message()
{
var factory = new ConnectionFactory();
var queue = "temperature-logs";
factory.UserName = _configuration["AMQP:username"];
factory.Password = _configuration["AMQP:password"];
factory.HostName = _configuration["AMQP:host"];
factory.Port = Convert.ToInt32(_configuration["AMQP:port"]);
// Connecting to our rabbitmq and after that it create's a channel where you can connect to a queue
using var conn = await factory.CreateConnectionAsync();
Console.WriteLine("AMQPClient connected");
using var channel = await conn.CreateChannelAsync();
// Here we connect to the queue through the channel that got created earlier
await channel.QueueDeclareAsync(queue: queue, durable: false, exclusive: false, autoDelete: false);
Console.WriteLine($"{queue} connected");
await Connect();
// Everytime a message is recieved from the queue it goes into this consumer.ReceivedAsync
var consumer = new AsyncEventingBasicConsumer(channel);
var consumer = new AsyncEventingBasicConsumer(_channel);
consumer.ReceivedAsync += (model, ea) =>
{
Console.WriteLine("Received application message.");
@ -73,9 +67,44 @@ namespace Api.AMQPReciever
};
// Consumes the data in the queue
await channel.BasicConsumeAsync(queue, true, consumer);
await _channel.BasicConsumeAsync(_queue, true, consumer);
while (true);
await Dispose();
}
// Disconnects from rabbitMQ
private async Task<bool> Dispose()
{
await _channel.CloseAsync();
await _conn.CloseAsync();
await _channel.DisposeAsync();
await _conn.DisposeAsync();
return true;
}
// Connects to rabbitMQ
private async Task<bool> Connect()
{
// Creating a new connection to rabbitMQ
_conn = await _factory.CreateConnectionAsync();
Console.WriteLine("AMQPClient connected");
_channel = await _conn.CreateChannelAsync();
// Here we connect to the queue through the channel that got created earlier
await _channel.QueueDeclareAsync(queue: _queue, durable: false, exclusive: false, autoDelete: false);
Console.WriteLine($"{_queue} connected");
return true;
}
// The info for the factory
private void InitFactory()
{
_factory.UserName = _configuration["AMQP:username"];
_factory.Password = _configuration["AMQP:password"];
_factory.HostName = _configuration["AMQP:host"];
_factory.Port = Convert.ToInt32(_configuration["AMQP:port"]);
}
}
}

View File

@ -49,7 +49,7 @@ namespace Api.BusinessLogic
string salt = Guid.NewGuid().ToString();
string hashedPassword = ComputeHash(user.Password, SHA256.Create(), salt);
user.Salt = salt;
user.Password = hashedPassword;
@ -74,7 +74,9 @@ namespace Api.BusinessLogic
if (user.Password == hashedPassword)
{
var token = GenerateJwtToken(user);
return new OkObjectResult(new { token, user.UserName, user.Id });
user.RefreshToken = Guid.NewGuid().ToString();
_dbAccess.UpdatesRefreshToken(user.RefreshToken, user.Id);
return new OkObjectResult(new { token, user.UserName, user.Id, refreshToken = user.RefreshToken });
}
return new ConflictObjectResult(new { message = "Invalid password" });
@ -119,6 +121,13 @@ namespace Api.BusinessLogic
return await _dbAccess.DeleteUser(userId);
}
public async Task<IActionResult> RefreshToken(string refreshToken)
{
User user = await _dbAccess.ReadUser(refreshToken);
if (user == null) { return new ConflictObjectResult(new { message = "Could not match refreshtoken" }); }
return new OkObjectResult(GenerateJwtToken(user));
}
/// <summary>
/// Generates a hash from a salt and input using the algorithm that is provided
/// </summary>

View File

@ -51,5 +51,11 @@ namespace Api.Controllers
return await _userLogic.DeleteUser(userId);
}
[HttpGet("RefreshToken")]
public async Task<IActionResult> RefreshToken(string refreshToken)
{
return await _userLogic.RefreshToken(refreshToken);
}
}
}

View File

@ -75,6 +75,20 @@ namespace Api.DBAccess
return await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
}
// Returns a user according to refreshToken
public async Task<User> ReadUser(string refreshToken)
{
return await _context.Users.FirstOrDefaultAsync(u => u.RefreshToken == refreshToken);
}
public async void UpdatesRefreshToken(string refreshToken, int userId)
{
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == userId);
user.RefreshToken = refreshToken;
user.RefreshTokenExpiresAt = DateTime.Now.AddDays(7);
}
/// <summary>
/// Updates the user in the database
/// </summary>

View File

@ -0,0 +1,138 @@
// <auto-generated />
using System;
using Api;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Migrations
{
[DbContext(typeof(DBContext))]
[Migration("20250327084557_AddedRefreshTokenToUser")]
partial class AddedRefreshTokenToUser
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.3");
modelBuilder.Entity("Api.Models.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("ReferenceId")
.HasColumnType("TEXT");
b.Property<double>("TempHigh")
.HasColumnType("REAL");
b.Property<double>("TempLow")
.HasColumnType("REAL");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Devices");
});
modelBuilder.Entity("Api.Models.TemperatureLogs", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("Date")
.HasColumnType("TEXT");
b.Property<int?>("DeviceId")
.HasColumnType("INTEGER");
b.Property<double>("TempHigh")
.HasColumnType("REAL");
b.Property<double>("TempLow")
.HasColumnType("REAL");
b.Property<double>("Temperature")
.HasColumnType("REAL");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.ToTable("TemperatureLogs");
});
modelBuilder.Entity("Api.Models.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("RefreshToken")
.HasColumnType("TEXT");
b.Property<DateTime>("RefreshTokenExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Salt")
.HasColumnType("TEXT");
b.Property<string>("UserName")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Api.Models.Device", b =>
{
b.HasOne("Api.Models.User", null)
.WithMany("Devices")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Api.Models.TemperatureLogs", b =>
{
b.HasOne("Api.Models.Device", null)
.WithMany("Logs")
.HasForeignKey("DeviceId");
});
modelBuilder.Entity("Api.Models.Device", b =>
{
b.Navigation("Logs");
});
modelBuilder.Entity("Api.Models.User", b =>
{
b.Navigation("Devices");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Migrations
{
/// <inheritdoc />
public partial class AddedRefreshTokenToUser : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "RefreshToken",
table: "Users",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "RefreshTokenExpiresAt",
table: "Users",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RefreshToken",
table: "Users");
migrationBuilder.DropColumn(
name: "RefreshTokenExpiresAt",
table: "Users");
}
}
}

View File

@ -88,6 +88,12 @@ namespace Api.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("RefreshToken")
.HasColumnType("TEXT");
b.Property<DateTime>("RefreshTokenExpiresAt")
.HasColumnType("TEXT");
b.Property<string>("Salt")
.HasColumnType("TEXT");

View File

@ -12,6 +12,10 @@
public string? Salt { get; set; }
public string? RefreshToken { get; set; }
public DateTime RefreshTokenExpiresAt { get; set; }
public List<Device>? Devices { get; set; }
}
}

View File

@ -66,6 +66,29 @@ namespace Api
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
// Configure Swagger to use Bearer token authentication
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header using the Bearer scheme",
Type = SecuritySchemeType.Http,
Scheme = "bearer"
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] { }
}
});
});
}