diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16ac8e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vscode +appsettings.Development.json +*.json +bin +obj diff --git a/Controllers/ContactManagement.cs b/Controllers/ContactManagement.cs new file mode 100644 index 0000000..36f0630 --- /dev/null +++ b/Controllers/ContactManagement.cs @@ -0,0 +1,67 @@ +using TextSender_API.Models; + +namespace TextSender_API.Controllers; + +public class ContactManagement +{ + #region Fields + private IConfiguration _config; + #endregion + + + #region Constructors + public ContactManagement(IConfiguration config) + { + this._config = config; + } + #endregion + + + #region Methods + // Queues the contact file to be processed latter + public ContactCreationQueue QueueContact(IFormFile file) + { + var queue = new ContactCreationQueue(); + queue.Processed = false; + var directory = this._config["Paths:ContactQueueDirectory"]; + + if (directory!.ElementAt(directory!.Length - 1) != '/') + { + directory.Append('/'); + } + + if (!System.IO.Directory.Exists(directory)) + { + queue.Status = "Missing directory"; + // Directory does not exist + return queue; + } + + var length = 16; + var chars = "ABCDEF0123456789"; + var random = new Random(); + var filename = new string(Enumerable.Repeat(chars, length).Select(s => + s[random.Next(s.Length)]).ToArray()); + var extension = ".txt"; + + var contactPath = $"{directory}/{filename}{extension}"; + queue.Filepath = contactPath; + + if (System.IO.File.Exists(contactPath)) + { + // Already exists + queue.Status = "File exists"; + return queue; + } + + queue.Status = "Created"; + + using (var filestream = new FileStream(contactPath, FileMode.Create)) + { + file.CopyTo(filestream); + } + + return queue; + } + #endregion +} \ No newline at end of file diff --git a/Controllers/ContactQueueCreationController.cs b/Controllers/ContactQueueCreationController.cs new file mode 100644 index 0000000..4f9ae4a --- /dev/null +++ b/Controllers/ContactQueueCreationController.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +using TextSender_API.Models; +using TextSender_API.Repositories; + +namespace TextSender_API.Controllers; + +[Authorize] +[ApiController] +[Route("api/v1/contact/queue")] +public class ContactQueueCreationController : ControllerBase +{ + #region Fields + private IConfiguration _config; + private readonly ILogger _logger; + #endregion + + + #region Constructors + + public ContactQueueCreationController(ILogger logger, IConfiguration config) + { + this._logger = logger; + this._config = config; + } + #endregion + + #region Methods + [HttpPost("upload"), DisableRequestSizeLimit] + public IActionResult Upload([FromForm(Name = "file")] List contactFile, + [FromForm(Name = "user_id")] string userID) + { + // Save the file on the filesystem to be processed by a separate + // system + var response = new CreateQueueResponse(); + var connString = this._config.GetConnectionString("MongoDBURI"); + var userRepo = new UsersRepository(connString!); + var queueRepo = new ContactCreationQueueRepository(connString!); + + try + { + var ctMgr = new ContactManagement(this._config); + var user = userRepo.Exists(userID); + + if (!user.Item1) + { + return NotFound(response); + } + + foreach (var file in contactFile) + { + var queue = ctMgr.QueueContact(file); + queue.UserId = user.Item2.Id; + queue.DateCreated = DateTime.Now; + queueRepo.Create(queue); + + response.Data.Add(queue); + } + } + catch (Exception ex) + { + this._logger.LogError($"An error occurred: {ex.Message}"); + } + + return Ok(response); + } + #endregion + + + #region Responses + public class CreateQueueResponse + { + #region Properties + [JsonProperty("data")] + public List Data { get; set; } + #endregion + + #region Constructors + public CreateQueueResponse() + { + this.Data = new List(); + } + #endregion + } + #endregion +} \ No newline at end of file diff --git a/Controllers/ContactsController.cs b/Controllers/ContactsController.cs new file mode 100644 index 0000000..9d492d8 --- /dev/null +++ b/Controllers/ContactsController.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +using TextSender_API.Models; +using TextSender_API.Repositories; + +namespace TextSender_API.Controllers; + +[Authorize] +[ApiController] +[Route("api/v1/contact")] +public class ContactsController : ControllerBase +{ + #region Fields + private IConfiguration _config; + private readonly ILogger _logger; + #endregion + + + #region Constructors + + public ContactsController(ILogger logger, IConfiguration config) + { + this._logger = logger; + this._config = config; + } + #endregion + + #region Methods + [HttpPost("new"), DisableRequestSizeLimit] + public IActionResult CreateContact([FromBody] Contact contact) + { + var response = new CreateContactResponse(); + var connString = this._config.GetConnectionString("MongoDBURI"); + var userRepo = new UsersRepository(connString!); + var contactsRepo = new ContactsRepository(connString!); + + try + { + var contacts = contactsRepo.RetrieveAll(); + var users = userRepo.RetrieveAllUsers(); + + if (users.Count() == 0) + { + return BadRequest(response); + } + + if (contacts.Count() > 0) + { + if (contacts.Exists(c => c.PhoneNumber!.Equals(contact.PhoneNumber)) || + string.IsNullOrEmpty(contact.UserID)) + { + // The Contact already exists + return BadRequest(response); + } + } + + if (!userRepo.Exists(contact.UserID!).Item1) + { + return BadRequest(response); + } + + contact.DateCreated = DateTime.Now; + contactsRepo.Create(contact); + response.Data.Add(contact); + } + catch (Exception ex) + { + this._logger.LogError($"An error occurred: {ex.Message}"); + } + + return Ok(response); + } + #endregion + + + #region Responses + public class CreateContactResponse + { + #region Properties + [JsonProperty("data")] + public List Data { get; set; } + #endregion + + #region Constructors + public CreateContactResponse() + { + this.Data = new List(); + } + #endregion + } + #endregion +} diff --git a/Controllers/PasswordVerification.cs b/Controllers/PasswordVerification.cs new file mode 100644 index 0000000..0eac000 --- /dev/null +++ b/Controllers/PasswordVerification.cs @@ -0,0 +1,64 @@ +using System.Security.Cryptography; +using System.Text; + +using TextSender_API.Models; +using TextSender_API.Repositories; + +namespace TextSender_API.Controllers; + +public class PasswordVerification +{ + #region Fiends + private UsersRepository _userRepo; + private SaltRepository _saltRepo; + private int _keySize = 64; + private int _iterations = 350000; + #endregion + + #region Constructors + public PasswordVerification(UsersRepository userRepo, SaltRepository saltRepo) + { + this._userRepo = userRepo; + this._saltRepo = saltRepo; + } + #endregion + + #region Methods + public bool VerifyPassword(User user, string password) + { + var salt = this._saltRepo.Retrieve(user.Id!); + var hashedPassword = this.HashPassword(user, salt, password); + + return hashedPassword.Equals(user.Password); + } + public Salt CreateSalt(User user) + { + var salt = new Salt(); + salt.Key = RandomNumberGenerator.GetBytes(this._keySize); + + return salt; + } + + public string HashPassword(User user, Salt salt, string password = "") + { + var hashAlgorithm = HashAlgorithmName.SHA512; + var dbHashedPassword = user.Password; + + if (!string.IsNullOrEmpty(password)) + { + user.Password = password; + } + + var hash = Rfc2898DeriveBytes.Pbkdf2( + Encoding.UTF8.GetBytes(user.Password!), + salt.Key!, + this._iterations, + hashAlgorithm, + this._keySize); + + user.Password = dbHashedPassword; + + return Convert.ToHexString(hash); + } + #endregion +} \ No newline at end of file diff --git a/Controllers/UsersController.cs b/Controllers/UsersController.cs new file mode 100644 index 0000000..c90b7aa --- /dev/null +++ b/Controllers/UsersController.cs @@ -0,0 +1,228 @@ +using System.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; + +using TextSender_API.Models; +using TextSender_API.Repositories; + +namespace TextSender_API.Controllers; + +[ApiController] +[Route("api/v1/user")] +public class UsersController : ControllerBase +{ + #region Fields + private readonly ILogger _logger; + private IConfiguration _config; + #endregion + + + #region Constructors + + public UsersController(ILogger logger, IConfiguration config) + { + this._logger = logger; + this._config = config; + } + #endregion + + #region Methods + private string AllScopes() + { + var allScopes = new List() + { + "create:contact" + }; + + var scopes = string.Empty; + + for (var i = 0; i < allScopes.Count; i++) + { + if (i == allScopes.Count - 1) + { + scopes += allScopes[i]; + } + else + { + scopes += allScopes[i] + " "; + } + } + + return scopes; + } + + private List Payload() + { + var expLimit = 30; + var currentDate = DateTime.Now; + var expiredDate = currentDate.AddMinutes(expLimit); + var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds); + var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds); + var issuer = this._config["JWT:Issuer"]; + var audience = this._config["JWT:Audience"]; + var subject = this._config["JWT:Subject"]; + + var claim = new List() + { + new System.Security.Claims.Claim("scope", AllScopes(), "string"), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Exp, expiredDate.ToString(), "string"), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience!), + new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer!), + new Claim(JwtRegisteredClaimNames.Sub, subject!) + }; + + return claim; + } + + [HttpPost("login")] + public IActionResult Login([FromBody] User userRequest) + { + var response = new LoginResponse(); + + try + { + var connString = this._config.GetConnectionString("MongoDBURI"); + var userRepo = new UsersRepository(connString!); + var saltRepo = new SaltRepository(connString!); + var pwdVerify = new PasswordVerification(userRepo, saltRepo); + var result = userRepo.Exists(userRequest); + + if (result.Item1 && pwdVerify.VerifyPassword(result.Item2, userRequest.Password!)) + { + var user = result.Item2; + var token = new Token(); + token.Issued = DateTime.Now; + token.UserId = user.Id; + token.TokenType = "JWT"; + + var payload = Payload(); + payload.Add(new System.Security.Claims.Claim("user_id", user.Id!, ClaimValueTypes.String)); + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this._config["JWT:Secret"]!)); + var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var tok = new JwtSecurityToken( + this._config["JWT:Issuer"], + this._config["JWT:Audience"], + payload, + expires: DateTime.UtcNow.AddMinutes(30), + signingCredentials: signIn); + + token.AccessToken = new JwtSecurityTokenHandler().WriteToken(tok); + + var expClaim = payload.FirstOrDefault(cl => + { + return cl.Type.Equals("exp"); + }); + + if (expClaim != null) + { + var expiredDate = DateTime.Parse(expClaim!.Value); + var exp = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds); + } + + response.Data.Add(token); + Ok(response); + } + else + { + return NotFound(response); + } + } + catch (Exception ex) + { + this._logger.LogError($"An error occurred: {ex.Message}"); + } + + return Ok(response); + } + + [HttpPost("register"), DisableRequestSizeLimit] + public IActionResult Upload([FromBody] User userRequest) + { + var response = new RegisterUserResponse(); + + try + { + var connString = this._config.GetConnectionString("MongoDBURI"); + var userRepo = new UsersRepository(connString!); + var saltRepo = new SaltRepository(connString!); + var allUsers = userRepo.RetrieveAllUsers(); + var result = allUsers.Exists(ea => ea.Username!.Equals(userRequest.Username)); + + if (!result) + { + var pwdVerify = new PasswordVerification(userRepo, saltRepo); + var salt = pwdVerify.CreateSalt(userRequest); + var hashedPassword = pwdVerify.HashPassword(userRequest, salt); + + this._logger.LogInformation("Creating user"); + userRequest.Password = hashedPassword; + userRequest.DateCreated = DateTime.Now; + userRepo.CreateUser(userRequest); + + response.Data.Add(userRequest); + + salt.UserId = userRequest.Id; + saltRepo.Create(salt); + } + else + { + this._logger.LogInformation("User already exists"); + } + } + catch (Exception ex) + { + this._logger.LogError($"An error occurred: {ex.Message}"); + } + + return Ok(response); + } + #endregion + + #region Responses + public class LoginResponse + { + #region Properties + [JsonProperty("data")] + public List Data { get; set; } + #endregion + + #region Constructors + public LoginResponse() + { + this.Data = new List(); + } + #endregion + + #region Methods + #endregion + } + + public class RegisterUserResponse + { + #region Properties + [JsonProperty("data")] + public List Data { get; set; } + #endregion + + #region Constructors + public RegisterUserResponse() + { + this.Data = new List(); + } + #endregion + + #region Methods + #endregion + } + #endregion +} diff --git a/Models/Contact.cs b/Models/Contact.cs new file mode 100644 index 0000000..72da9a0 --- /dev/null +++ b/Models/Contact.cs @@ -0,0 +1,53 @@ +using System; + +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json; + +namespace TextSender_API.Models; + +public class Contact +{ +#region Properties + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [JsonProperty("id")] + public string? Id { get; set; } + [BsonElement("firstname")] + [JsonProperty("firstname")] + public string? Firstname { get; set; } + [BsonElement("lastname")] + [JsonProperty("lastname")] + public string? Lastname { get; set; } + [BsonElement("phonenumber")] + [JsonProperty("phonenumber")] + public string? PhoneNumber { get; set; } + [BsonElement("date_created")] + [JsonProperty("date_created")] + public DateTime DateCreated { get; set; } + // [BsonRepresentation(BsonType.ObjectId)] + [BsonElement("user_id")] + [JsonProperty("user_id")] + public string? UserID { get; set; } +#endregion +} + + +public class ContactCreationQueue +{ + #region Properties + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + [BsonElement("filepath")] + public string? Filepath { get; set; } + [BsonElement("status")] + public string? Status { get; set; } + [BsonElement("user_id")] + public string? UserId { get; set; } + [BsonElement("date_created")] + public DateTime? DateCreated { get; set; } + [BsonElement("processed")] + public bool? Processed { get; set; } + #endregion +} \ No newline at end of file diff --git a/Models/Text.cs b/Models/Text.cs new file mode 100644 index 0000000..8a5e8b0 --- /dev/null +++ b/Models/Text.cs @@ -0,0 +1,14 @@ +using System; + +namespace TextSender_API.Models; + +public class Text +{ +#region Properties + public int TextID { get; set; } + public string? Subject { get; set; } + public string? Message { get; set; } + public int UserID { get; set; } + public DateTime DateCreated { get; set; } +#endregion +} diff --git a/Models/TextQueue.cs b/Models/TextQueue.cs new file mode 100644 index 0000000..9d7327b --- /dev/null +++ b/Models/TextQueue.cs @@ -0,0 +1,16 @@ +using System; + +namespace TextSender_API.Models; + +public class TextQueue +{ +#region Properties + public int TextQueueID { get; set; } + public string? Status { get; set; } + public string? ContactPhoneNumber { get; set; } + public int TextID { get; set; } + public int UserID { get; set; } + public DateTime DateCreated { get; set; } + public DateTime ExecutionDate { get; set; } +#endregion +} diff --git a/Models/Token.cs b/Models/Token.cs new file mode 100644 index 0000000..afbe858 --- /dev/null +++ b/Models/Token.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace TextSender_API.Models; + +public class Token +{ + #region Properties + [JsonProperty("access_token")] + public string? AccessToken { get; set; } + [JsonProperty("token_type")] + public string? TokenType { get; set; } + [JsonProperty("issued")] + public DateTime? Issued { get; set; } + [JsonProperty("id")] + public string? UserId { get; set; } + #endregion +} \ No newline at end of file diff --git a/Models/User.cs b/Models/User.cs new file mode 100644 index 0000000..9cbf051 --- /dev/null +++ b/Models/User.cs @@ -0,0 +1,46 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Newtonsoft.Json; + +namespace TextSender_API.Models; + +public class User +{ +#region Properties + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + [JsonProperty("id")] + public string? Id { get; set; } + [BsonElement("firstname")] + [JsonProperty("firstname")] + public string? Firstname { get; set; } + [BsonElement("lastname")] + [JsonProperty("lastname")] + public string? Lastname { get; set; } + [BsonElement("phonenumber")] + [JsonProperty("phone_number")] + public string? PhoneNumber { get; set; } + [BsonElement("username")] + [JsonProperty("username")] + public string? Username { get; set; } + [BsonElement("password")] + [JsonProperty("password")] + public string? Password { get; set; } + [BsonElement("datecreated")] + [JsonProperty("date_created")] + public DateTime? DateCreated { get; set; } +#endregion +} + +public class Salt +{ + #region Properties + [BsonId] + [BsonRepresentation(BsonType.ObjectId)] + public string? Id { get; set; } + [BsonElement("key")] + public byte[]? Key { get; set; } + [BsonElement("user_id")] + public string? UserId { get; set; } + #endregion +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..66aca82 --- /dev/null +++ b/Program.cs @@ -0,0 +1,63 @@ +using System.Text; + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; + +using TextSender_API.Repositories; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +builder.Services.AddControllers().AddNewtonsoftJson(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => +{ + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new TokenValidationParameters() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidAudience = builder.Configuration["JWT:Audience"], + ValidIssuer = builder.Configuration["JWT:Issuer"], + RequireExpirationTime = false, + ValidateIssuerSigningKey = false, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JWT:Secret"]!)) + }; +}); + +// var connectionString = builder.Configuration.GetConnectionString("MongoDBURI"); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + + +app.UseHttpsRedirection(); + +app.UseAuthentication(); +app.UseRouting(); +app.UseAuthorization(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +// app.MapControllers(); + +app.Run(); diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..e609dd5 --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22514", + "sslPort": 44361 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5168", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7154;http://localhost:5168", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/README.md b/README.md index 33e7395..b8543de 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,11 @@ An API used to send texts messages to multiple contacts. ## Getting started Installing dependencies -`npm install` +```BASH +dotnet restore +``` Running server -`node app.js` +```BASH +dotnet run +``` diff --git a/Repositories/BaseRepository.cs b/Repositories/BaseRepository.cs new file mode 100644 index 0000000..e437670 --- /dev/null +++ b/Repositories/BaseRepository.cs @@ -0,0 +1,11 @@ + +namespace TextSender_API.Repositories; + +public class BaseRepository +{ + #region Fields + protected string? _connectionString; + protected string _databaseName = "textSender"; + protected string? _tableName; + #endregion +} \ No newline at end of file diff --git a/Repositories/ContactRepository.cs b/Repositories/ContactRepository.cs new file mode 100644 index 0000000..b4d9ceb --- /dev/null +++ b/Repositories/ContactRepository.cs @@ -0,0 +1,131 @@ +using MongoDB.Driver; + +using TextSender_API.Models; + +namespace TextSender_API.Repositories; + + +public class ContactCreationQueueRepository : BaseRepository +{ + #region Fields + private readonly IMongoCollection _bookCollection; + #endregion + + + #region Constructors + public ContactCreationQueueRepository(string connectionString) + { + this._connectionString = connectionString; + this._tableName = "contactCreationQueue"; + var client = this.InitializeClient(); + this._bookCollection = client.GetDatabase(this._databaseName).GetCollection(this._tableName); + } + #endregion + + #region Metods + public Tuple Exists(string id) + { + var allQueue = this.RetrieveAll(); + var result = allQueue.FirstOrDefault(q => q.Id!.Equals(id)); + + if (result != null) + { + return new Tuple(true, result!); + } + else + { + return new Tuple(false, new ContactCreationQueue()); + } + } + + public void Create(ContactCreationQueue queue) + { + this._bookCollection.InsertOne(queue); + } + + public List RetrieveAll() + { + return this._bookCollection.Find(_ => true).ToList(); + } + + public void Update(ContactCreationQueue queue) + { + var filter = Builders.Filter.Eq(q => q.Id, queue.Id); + this._bookCollection.ReplaceOne(filter, queue); + } + + public void Delete(ContactCreationQueue queue) + { + var filter = Builders.Filter.Eq(q => q.Id, queue.Id); + this._bookCollection.DeleteOne(filter); + } + + private MongoClient InitializeClient() + { + return new MongoClient(this._connectionString); + } + #endregion +} + + +public class ContactsRepository : BaseRepository +{ + #region Fields + private readonly IMongoCollection _bookCollection; + #endregion + + + #region Constructors + public ContactsRepository(string connectionString) + { + this._connectionString = connectionString; + this._tableName = "contacts"; + var client = this.InitializeClient(); + this._bookCollection = client.GetDatabase(this._databaseName).GetCollection(this._tableName); + } + #endregion + + #region Metods + public Tuple Exists(string id) + { + var allContacts= this.RetrieveAll(); + var result = allContacts.FirstOrDefault(c => c.Id!.Equals(id)); + + if (result != null) + { + return new Tuple(true, result!); + } + else + { + return new Tuple(false, new Contact()); + } + } + + public void Create(Contact contact) + { + this._bookCollection.InsertOne(contact); + } + + public List RetrieveAll() + { + return this._bookCollection.Find(_ => true).ToList(); + } + + public void Update(Contact contact) + { + var filter = Builders.Filter.Eq(c => c.Id, contact.Id); + this._bookCollection.ReplaceOne(filter, contact); + } + + public void Delete(Contact contact) + { + var filter = Builders.Filter.Eq(c => c.Id, contact.Id); + this._bookCollection.DeleteOne(filter); + } + + private MongoClient InitializeClient() + { + return new MongoClient(this._connectionString); + } + #endregion +} \ No newline at end of file diff --git a/Repositories/UsersRepository.cs b/Repositories/UsersRepository.cs new file mode 100644 index 0000000..c9a86a1 --- /dev/null +++ b/Repositories/UsersRepository.cs @@ -0,0 +1,136 @@ +using MongoDB.Driver; + +using TextSender_API.Models; + +namespace TextSender_API.Repositories; + + +public class UsersRepository : BaseRepository +{ + #region Fields + private readonly IMongoCollection _bookCollection; + #endregion + + + #region Constructors + public UsersRepository(string connectionString) + { + this._connectionString = connectionString; + this._tableName = "users"; + var client = this.InitializeClient(); + this._bookCollection = client.GetDatabase(this._databaseName).GetCollection(this._tableName); + } + #endregion + + #region Metods + public Tuple Exists(User user) + { + var allUsers = this.RetrieveAllUsers(); + var result = allUsers.FirstOrDefault(ea => ea.Username!.Equals(user.Username)); + + if (result != null) + { + return new Tuple(true, result!); + } + else + { + return new Tuple(false, new User()); + } + } + + public Tuple Exists(string userId) + { + var allUsers = this.RetrieveAllUsers(); + var result = allUsers.FirstOrDefault(ea => ea.Id!.Equals(userId)); + + if (result != null) + { + return new Tuple(true, result!); + } + else + { + return new Tuple(false, new User()); + } + } + + public void CreateUser(User user) + { + this._bookCollection.InsertOne(user); + } + + public List RetrieveAllUsers() + { + return this._bookCollection.Find(_ => true).ToList(); + } + + public void Update(User user) + { + var filter = Builders.Filter.Eq(u => u.Id, user.Id); + this._bookCollection.ReplaceOne(filter, user); + } + + public void Delete(User user) + { + var filter = Builders.Filter.Eq(u => u.Id, user.Id); + this._bookCollection.DeleteOne(filter); + } + + private MongoClient InitializeClient() + { + return new MongoClient(this._connectionString); + } + #endregion +} + + +public class SaltRepository : BaseRepository +{ + #region Fields + private readonly IMongoCollection _bookCollection; + #endregion + + + #region Constructors + public SaltRepository(string connectionString) + { + this._connectionString = connectionString; + this._tableName = "salt"; + var client = this.InitializeClient(); + this._bookCollection = client.GetDatabase(this._databaseName).GetCollection(this._tableName); + } + #endregion + + #region Metods + public void Create(Salt salt) + { + this._bookCollection.InsertOne(salt); + } + + public List RetrieveAll() + { + return this._bookCollection.Find(_ => true).ToList(); + } + + public Salt Retrieve(string userId) + { + return this._bookCollection.Find(s => s.UserId!.Equals(userId)).FirstOrDefault(); + } + + public void Update(Salt salt) + { + var filter = Builders.Filter.Eq(u => u.Id, salt.Id); + this._bookCollection.ReplaceOne(filter, salt); + } + + public void Delete(Salt salt) + { + var filter = Builders.Filter.Eq(u => u.Id, salt.Id); + this._bookCollection.DeleteOne(filter); + } + + private MongoClient InitializeClient() + { + return new MongoClient(this._connectionString); + } + #endregion +} \ No newline at end of file diff --git a/TextSender-API.csproj b/TextSender-API.csproj new file mode 100644 index 0000000..d4d3f0b --- /dev/null +++ b/TextSender-API.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + enable + enable + TextSender_API + + + + + + + + + + + + + diff --git a/app.js b/app.js deleted file mode 100644 index c9441eb..0000000 --- a/app.js +++ /dev/null @@ -1,74 +0,0 @@ -require('./models.js') - -const express = require('express') -const app = express() -const port = 3000 - -app.use(express.json()) - -// User Routes -app.post('/api/v1/user/new', (req, res) => { - res.send('Posting') - console.log(req.body) - - // const usr = new User() - - let username = req.body['username'] - - // usr.username = username - - console.log(`Username: ${username}`) -}) - -// User routes -// TODO: Not implemented -app.post('/api/v1/login', (req, res) => { - res.send('Implement'); -}) - -// Text Routes -// Add a new text -// TODO: Not implemented -app.post('/api/v1/text/new', (req, res) => { - res.send('Create new text') -}) - -// Retrieve texts -// TODO: Not implemented -app.get('/api/v1/text', (req, res) => { - res.send('Create new text') -}) - -// Queue a text for sending -// TODO: Not implemented -app.post('/api/v1/text/queue', (req, res) => { - res.send('Queue text') -}) - -// Create contact -// TODO: Not implemented -app.post('/api/v1/contacts/new', (req, res) => { - res.send('Creating contact') -}) - -// Retrieving contacts -// TODO: Not implemented -app.get('/api/v1/contacts', (req, res) => { - res.send('Retrieving contact') -}) - -// Editing a contact -// TODO: Not implemented -app.patch('/api/v1/contacts', (req, res) => { - res.send('Editing contact') -}) - -// Deleting a contact -// TODO: Not implemented -app.delete('/api/v1/contacts', (req, res) => { - res.send('Delete contact') -}) - -app.listen(port, () => { - console.log(`textsender-api listening on port ${port}`) -}) diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..e0a901f --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "JWT": { + "Audience": "", + "Issuer": "", + "Secret": "", + "Subject": "" + }, + "Paths": { + "ContactQueueDirectory": "/home/user/path/" + }, + "ConnectionStrings": { + "MongoDBURI": "mongodb+srv://:@cluster0.abc.mongodb.net/?retryWrites=true&w=majority" + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..8a3238e --- /dev/null +++ b/appsettings.json @@ -0,0 +1,21 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "JWT": { + "Audience": "", + "Issuer": "", + "Secret": "", + "Subject": "" + }, + "Paths": { + "ContactQueueDirectory": "/home/user/path/" + }, + "ConnectionStrings": { + "MongoDBURI": "mongodb+srv://:@cluster0.abc.mongodb.net/?retryWrites=true&w=majority" + } +} diff --git a/models.js b/models.js deleted file mode 100644 index 53d57f2..0000000 --- a/models.js +++ /dev/null @@ -1,14 +0,0 @@ -const mongoose = require('mongoose'); - -const Schema = mongoose.Schema; -const ObjectId = Schema.ObjectId; - -const User = new Schema({ - userID: { type: String }, - firstName: { type: String }, - lastName: { type: String }, - phoneNumber: { type: String }, - username: { type: String }, - password: { type: String }, - dateCreated: { type: Date, default: Date.now } -}); diff --git a/package.json b/package.json deleted file mode 100644 index 39db95f..0000000 --- a/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "textsender-api", - "version": "0.1.0", - "description": "Web API for queueing text message notifications", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "kdeng00", - "license": "ISC", - "dependencies": { - "express": "^4.18.2", - "mongodb": "^5.3.0", - "mongoose": "^7.0.5" - } -} diff --git a/textsender-api.sln b/textsender-api.sln new file mode 100644 index 0000000..b57d8ea --- /dev/null +++ b/textsender-api.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TextSender-API", "TextSender-API.csproj", "{AD184BBA-C715-4112-95A9-530F05323BEE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AD184BBA-C715-4112-95A9-530F05323BEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AD184BBA-C715-4112-95A9-530F05323BEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AD184BBA-C715-4112-95A9-530F05323BEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AD184BBA-C715-4112-95A9-530F05323BEE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {70126048-DECC-43EE-9956-2CEF937B7AA1} + EndGlobalSection +EndGlobal