diff --git a/backend/user-service/user-service-docs.md b/backend/user-service/user-service-docs.md index 9be9bcf..6959bba 100644 --- a/backend/user-service/user-service-docs.md +++ b/backend/user-service/user-service-docs.md @@ -1 +1,215 @@ -Nothing \ No newline at end of file +# 🛡️ User Service API + +Проєкт розроблений на **C# (.NET 8)** з використанням **ASP.NET Core Web API**, **Kestrel**, **PostgreSQL**, **JWT-аутентифікації**, контейнеризований у **Docker**. + +## 🔐 Загальні вимоги до запитів + +Кожен запит до API повинен містити заголовок авторизації з JWT-токеном: + +### 🔸 Authorization Header + +```http +Authorization: Bearer +``` + +## 🔴 Помилки + +### 401 Unauthorized + +```json +{ + "message": "Unautorized error" +} +``` + +### 500 Internal Server Error + +```json +{ + "message": "Internal error", + "error": "" +} +``` + +## 📘 Ендпоїнти + +### ✅ POST /api/admin/get_users + +Отримання **всіх користувачів** з бази. + +#### 🔹 Request Body + +```json +null +``` + +#### 🔹 Response 200 OK + +```json +{ + "users": [ + { + "id": "", + "createdAt": "", + "updatedAt": "", + "lastLoginAt": "", + "isActive": "", + "password": "", + "roleId": "", + "verificationStatus": "", + "organizationId": "", + "email": "", + "fullName": "", + "phone": "", + "avatarUrl": "", + "govId": "" + } + ] +} +``` + +### ✅ POST /api/admin/get_user + +Отримання **одного користувача** по `id`. + +#### 🔹 Request Body + +```json +{ + "id": "" +} +``` + +#### 🔹 Response 200 OK + +```json +{ + "createdUser": { + "id": "", + "createdAt": "", + "updatedAt": "", + "lastLoginAt": "", + "isActive": "", + "password": "", + "roleId": "", + "verificationStatus": "", + "organizationId": "", + "email": "", + "fullName": "", + "phone": "", + "avatarUrl": "", + "govId": "" + } +} +``` + +### ✅ POST /api/admin/user_add + +Додавання **нового користувача**. + +#### 🔹 Request Body + +```json +{ + "fullName": "", + "email": "", + "password": "" +} +``` + +#### 🔹 Response 200 OK + +```json +{ + "user": [ + { + "id": "", + "createdAt": "", + "updatedAt": "", + "lastLoginAt": "", + "isActive": "", + "password": "", + "roleId": "", + "verificationStatus": "", + "organizationId": "", + "email": "", + "fullName": "", + "phone": "", + "avatarUrl": "", + "govId": "" + } + ] +} +``` + +#### 🔻 Помилки + +##### 400 Bad Request + +```json +{ + "message": "Full name is required" +} +``` + +```json +{ + "message": "Email is required" +} +``` + +```json +{ + "message": "Password is required" +} +``` + +##### 409 Conflict + +```json +{ + "message": "Email already exists" +} +``` + +### ✅ POST /api/admin/user_change + +Зміна **даних користувача** за `id`. + +#### 🔹 Request Body + +```json +{ + "id": "", + "createdAt": "", + "updatedAt": "", + "lastLoginAt": "", + "isActive": "", + "password": "", + "roleId": "", + "verificationStatus": "", + "organizationId": "", + "email": "", + "fullName": "", + "phone": "", + "avatarUrl": "", + "govId": "" +} +``` + +#### 🔹 Response 200 OK + +```json +{ + "message": "Ok" +} +``` + +## 📦 Технології + +- C# / .NET 8 +- ASP.NET Core Web API +- PostgreSQL +- JWT (RS256) +- Docker +- Kestrel \ No newline at end of file diff --git a/backend/user-service/user-service/Dockerfile b/backend/user-service/user-service/Dockerfile deleted file mode 100644 index 2ac8c86..0000000 --- a/backend/user-service/user-service/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -WORKDIR /src -COPY . . -RUN dotnet publish -c Release -o /app - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 -WORKDIR /app -COPY --from=build /app . -ENTRYPOINT ["dotnet", "user-service.dll"] diff --git a/backend/user-service/user-service/Program.cs b/backend/user-service/user-service/Program.cs deleted file mode 100644 index ddb667a..0000000 --- a/backend/user-service/user-service/Program.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Hosting; - -var app = WebApplication.Create(args); -app.MapGet("/", () => "Hello from Kestrel!"); -app.Run(); diff --git a/backend/user-service/user-service/docker-compose.yml b/backend/user-service/user-service/docker-compose.yml new file mode 100644 index 0000000..2bf70c5 --- /dev/null +++ b/backend/user-service/user-service/docker-compose.yml @@ -0,0 +1,7 @@ +services: + user-service: + build: + context: . + dockerfile: docker/Dockerfile + ports: + - "883:80" diff --git a/backend/user-service/user-service/docker/Dockerfile b/backend/user-service/user-service/docker/Dockerfile new file mode 100644 index 0000000..bd2fc59 --- /dev/null +++ b/backend/user-service/user-service/docker/Dockerfile @@ -0,0 +1,17 @@ +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ./src/ . +RUN dotnet publish -c Release -o /app + +RUN useradd -u 8571 -r -g -0 -s /sbin/nologin \ + -c "Default Application User" postgres + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 +WORKDIR /app +COPY --from=build /app . + +COPY ./keys/public.pem ./public.pem + +USER 8571 + +ENTRYPOINT ["dotnet", "app.dll"] diff --git a/backend/user-service/user-service/keys/public.pem b/backend/user-service/user-service/keys/public.pem new file mode 100644 index 0000000..bddff5a --- /dev/null +++ b/backend/user-service/user-service/keys/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Ytzek1ZpurQJ+wCb5q5 +4IBg33MCA+QXbgQ3G1udE7f1EjRKlLmMOyedMVr5t5Ssyh6oQYMKjr/5akRfwoXm +JU4JjkdmS6slqho/vYQQoOB1+HNzeOIY8jMMrbVMLYnwGBH3DNgOFJUFQiPPJ/o9 ++GSvDwCbQ4cz1hSNKpywwAuHNowe7umC/9AzbOjSUfZcrh+qf2BbJg2Am5cYtdZV +h1PBXBiJCR+Bt+eEag8OUCSPBe4iOfyGttjPsLOHxObvlsuERuq6AowqvY0bam1L +tbHs4bZ7Jo0k7X0dYI9CriaeaNHGs/lBWfueGGJNlbplj9tZz9Qe/fIkK4NLN3qi +jQIDAQAB +-----END PUBLIC KEY----- diff --git a/backend/user-service/user-service/src/Controllers/Common/ControllerBaseAdminRequired.cs b/backend/user-service/user-service/src/Controllers/Common/ControllerBaseAdminRequired.cs new file mode 100644 index 0000000..66afdf2 --- /dev/null +++ b/backend/user-service/user-service/src/Controllers/Common/ControllerBaseAdminRequired.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +using Services.Token; + +namespace Controllers.Users.Common; + +[ApiController] +public abstract class ControllerBaseAdminRequired : ControllerBase, IActionFilter +{ + protected ITokenPacketProcessorService _tokenService; + + protected Guid UserId { get; private set; } + + protected ControllerBaseAdminRequired(ITokenPacketProcessorService tokenService) + { + _tokenService = tokenService; + } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (!_tokenService.TryValidateToken(context.HttpContext.Request, out var userId)) + { + //context.Result = new UnauthorizedObjectResult(new { message = "Invalid token" }); + + return; + } + + UserId = userId; + } + + public void OnActionExecuted(ActionExecutedContext context) { } +} diff --git a/backend/user-service/user-service/src/Controllers/ControllerAddUser.cs b/backend/user-service/user-service/src/Controllers/ControllerAddUser.cs new file mode 100644 index 0000000..d9d2061 --- /dev/null +++ b/backend/user-service/user-service/src/Controllers/ControllerAddUser.cs @@ -0,0 +1,49 @@ +using Controllers.Users.Common; + +using Microsoft.AspNetCore.Mvc; + +using Services.Token; +using Services.PasswordHashing; +using Services.Users; + +using Models.Users; + +namespace Controllers.Users; + +[ApiController] +[Route("api/admin/user_add")] +public class ControllerAddUser( + ITokenPacketProcessorService tokenService, + IPasswordHashingService passwordHasher, + AddUserService addUserService +) : ControllerBaseAdminRequired(tokenService) +{ + [HttpPost] + public IActionResult AddUser([FromBody] AddUserRequest req) + { + try + { + if (string.IsNullOrWhiteSpace(req.FullName)) return BadRequest(new { message = "Full name is required" }); + + if (string.IsNullOrWhiteSpace(req.Email)) return BadRequest(new { message = "Email is required" }); + + if (string.IsNullOrWhiteSpace(req.Password)) return BadRequest(new { message = "Password is required" }); + + if (addUserService.EmailExists(req.Email)) return Conflict(new { message = "Email already exists" }); + + var hash = passwordHasher.Hash(req.Password); + + var id = addUserService.CreateUser(req.FullName, req.Email, hash); + + var createdUser = addUserService.GetById(id); + + if (createdUser is null) return StatusCode(500, new { message = "Internal Error" }); + + return Ok(new { createdUser }); + } + catch (Exception ex) + { + return StatusCode(500, new { message = "Internal error", error = ex.Message }); + } + } +} diff --git a/backend/user-service/user-service/src/Controllers/ControllerChangeUser.cs b/backend/user-service/user-service/src/Controllers/ControllerChangeUser.cs new file mode 100644 index 0000000..2d29ac6 --- /dev/null +++ b/backend/user-service/user-service/src/Controllers/ControllerChangeUser.cs @@ -0,0 +1,32 @@ +using Controllers.Users.Common; + +using Microsoft.AspNetCore.Mvc; + +using Services.Token; +using Services.Users; + +using Models.Users; + +namespace Controllers.Users; + +[ApiController] +[Route("api/admin/user_change")] +public class ControllerChangeUser( + ITokenPacketProcessorService tokenService, + ChangeUserService changeUserService +) : ControllerBaseAdminRequired(tokenService) +{ + [HttpPost] + public IActionResult ChangeUser([FromBody] ChangeUserRequest req) + { + try + { + changeUserService.ChangeUser(req.Id, req.CreatedAt, req.Email, req.FullName, req.Phone, req.VerificationStatus); + return Ok(new { message = "User updated successfully" }); + } + catch (Exception ex) + { + return StatusCode(500, new { message = "Internal error", error = ex.Message }); + } + } +} diff --git a/backend/user-service/user-service/src/Controllers/ControllerGetUser.cs b/backend/user-service/user-service/src/Controllers/ControllerGetUser.cs new file mode 100644 index 0000000..4c16146 --- /dev/null +++ b/backend/user-service/user-service/src/Controllers/ControllerGetUser.cs @@ -0,0 +1,34 @@ +using Controllers.Users.Common; + +using Microsoft.AspNetCore.Mvc; + +using Services.Token; +using Services.Users; + +using Models.Users; + +namespace Controllers.Users; + +[ApiController] +[Route("api/admin/get_user")] +public class ControllerGetUser( + ITokenPacketProcessorService tokenService, + GetUserService userDataService +) : ControllerBaseAdminRequired(tokenService) + +{ + [HttpPost] + public IActionResult GetUser([FromBody] GetUserRequest req) + { + try + { + var user = userDataService.GetUser(req.Id); + return Ok(new { user }); + } + catch (Exception ex) + { + return StatusCode(500, new { message = "Internal error", error = ex.Message }); + } + } + +} diff --git a/backend/user-service/user-service/src/Controllers/ControllerGetUsers.cs b/backend/user-service/user-service/src/Controllers/ControllerGetUsers.cs new file mode 100644 index 0000000..2f49307 --- /dev/null +++ b/backend/user-service/user-service/src/Controllers/ControllerGetUsers.cs @@ -0,0 +1,32 @@ +using Controllers.Users.Common; + +using Microsoft.AspNetCore.Mvc; + +using Services.Token; +using Services.Users; + +namespace Controllers.Users; + +[ApiController] +[Route("api/admin/get_users")] +public class ControllerGetUsers( + ITokenPacketProcessorService tokenService, + GetUsersService userDataService +) : ControllerBaseAdminRequired(tokenService) + +{ + [HttpPost] + public IActionResult GetUsers() + { + try + { + var users = userDataService.GetAllUsers(); + return Ok(new { users }); + } + catch (Exception ex) + { + return StatusCode(500, new { message = "Internal error", error = ex.Message }); + } + } + +} diff --git a/backend/user-service/user-service/src/Models/Models.cs b/backend/user-service/user-service/src/Models/Models.cs new file mode 100644 index 0000000..0645cad --- /dev/null +++ b/backend/user-service/user-service/src/Models/Models.cs @@ -0,0 +1,24 @@ +namespace Models.Users; + +public record AddUserRequest(string FullName, string Email, string Password); + +public record ChangeUserRequest(Guid Id, DateTime CreatedAt, string Email, string FullName, string Phone, string VerificationStatus); + +public record GetUserRequest(Guid Id); + +public record UserDto( + Guid Id, + DateTime CreatedAt, + DateTime UpdatedAt, + DateTime? LastLoginAt, + bool IsActive, + string Password, + Guid RoleId, + string VerificationStatus, + Guid OrganizationId, + string Email, + string FullName, + string Phone, + string AvatarUrl, + string GovId +); \ No newline at end of file diff --git a/backend/user-service/user-service/src/Program.cs b/backend/user-service/user-service/src/Program.cs new file mode 100644 index 0000000..15a1449 --- /dev/null +++ b/backend/user-service/user-service/src/Program.cs @@ -0,0 +1,42 @@ +using Services.Auth; +using Services.Database; +using Services.PasswordHashing; +using Services.Token; +using Services.Users; + +var builder = WebApplication.CreateBuilder(args); + +builder.WebHost.UseUrls("http://0.0.0.0:80"); + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy + .AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); + }); +}); + +builder.Services.AddControllers(); + +builder.Services.AddScoped(); +builder.Services.AddSingleton(new DbService(builder.Configuration.GetConnectionString("Postgres")!)); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +var app = builder.Build(); + +app.UseCors(); + +app.UseRouting(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/backend/user-service/user-service/src/Services/Database/DbService.cs b/backend/user-service/user-service/src/Services/Database/DbService.cs new file mode 100644 index 0000000..f8fb827 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Database/DbService.cs @@ -0,0 +1,79 @@ +using Npgsql; + +using System.Data; + +namespace Services.Database; + +public sealed class DbService : IDbService +{ + private readonly string _connectionString; + + public DbService(string connectionString) + { + _connectionString = connectionString; + } + + public T? QuerySingle(string sql, Func map, object? parameters = null) + { + using var conn = new NpgsqlConnection(_connectionString); + conn.Open(); + + using var cmd = new NpgsqlCommand(sql, conn); + AddParameters(cmd, parameters); + + using var reader = cmd.ExecuteReader(); + return reader.Read() ? map(reader) : default; + } + + public IEnumerable Query(string sql, Func map, object? parameters = null) + { + using var conn = new NpgsqlConnection(_connectionString); + conn.Open(); + + using var cmd = new NpgsqlCommand(sql, conn); + AddParameters(cmd, parameters); + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + yield return map(reader); + } + } + + public int Execute(string sql, object? parameters = null) + { + using var conn = new NpgsqlConnection(_connectionString); + conn.Open(); + + using var cmd = new NpgsqlCommand(sql, conn); + AddParameters(cmd, parameters); + + return cmd.ExecuteNonQuery(); + } + + private void AddParameters(NpgsqlCommand cmd, object? parameters) + { + if (parameters == null) return; + + foreach (var prop in parameters.GetType().GetProperties()) + { + var name = "@" + prop.Name; + var value = prop.GetValue(parameters) ?? DBNull.Value; + + var param = cmd.Parameters.Add(name, GetDbType(value)); + param.Value = value; + } + } + + private NpgsqlTypes.NpgsqlDbType GetDbType(object value) => + value switch + { + Guid => NpgsqlTypes.NpgsqlDbType.Uuid, + byte[] => NpgsqlTypes.NpgsqlDbType.Bytea, + bool => NpgsqlTypes.NpgsqlDbType.Boolean, + int => NpgsqlTypes.NpgsqlDbType.Integer, + long => NpgsqlTypes.NpgsqlDbType.Bigint, + DateTime => NpgsqlTypes.NpgsqlDbType.Timestamp, + _ => NpgsqlTypes.NpgsqlDbType.Text + }; +} diff --git a/backend/user-service/user-service/src/Services/Database/Helpers/DbReader.cs b/backend/user-service/user-service/src/Services/Database/Helpers/DbReader.cs new file mode 100644 index 0000000..370d196 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Database/Helpers/DbReader.cs @@ -0,0 +1,66 @@ +using System.Data; + +namespace Services.Database.Helpers; + +public static class DbReader +{ + public static Guid _GetGuid(this IDataReader r, string name) + { + try + { + int i = r.GetOrdinal(name); + return r.IsDBNull(i) ? Guid.Empty : r.GetGuid(i); + } + catch { return Guid.Empty; } + } + + public static string _GetString(this IDataReader r, string name) + { + try + { + int i = r.GetOrdinal(name); + return r.IsDBNull(i) ? string.Empty : r.GetString(i); + } + catch { return string.Empty; } + } + + public static bool _GetBool(this IDataReader r, string name) + { + try + { + int i = r.GetOrdinal(name); + return !r.IsDBNull(i) && r.GetBoolean(i); + } + catch { return false; } + } + + public static byte[] _GetByteArray(this IDataReader r, string name) + { + try + { + object val = r[name]; + return val is byte[] data ? data : Array.Empty(); + } + catch { return Array.Empty(); } + } + + public static DateTime _GetDateTime(this IDataReader r, string name) + { + try + { + int i = r.GetOrdinal(name); + return r.IsDBNull(i) ? DateTime.MinValue : r.GetDateTime(i); + } + catch { return DateTime.MinValue; } + } + + public static DateTime? _GetNullableDateTime(this IDataReader r, string name) + { + try + { + int i = r.GetOrdinal(name); + return r.IsDBNull(i) ? null : r.GetDateTime(i); + } + catch { return null; } + } +} diff --git a/backend/user-service/user-service/src/Services/Database/IDbService.cs b/backend/user-service/user-service/src/Services/Database/IDbService.cs new file mode 100644 index 0000000..e6c99e1 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Database/IDbService.cs @@ -0,0 +1,12 @@ +using System.Data; + +namespace Services.Database; + +public interface IDbService +{ + T? QuerySingle(string sql, Func map, object? parameters = null); + + IEnumerable Query(string sql, Func map, object? parameters = null); + + int Execute(string sql, object? parameters = null); +} diff --git a/backend/user-service/user-service/src/Services/Password Hashing Service/IPasswordHashingService.cs b/backend/user-service/user-service/src/Services/Password Hashing Service/IPasswordHashingService.cs new file mode 100644 index 0000000..f406166 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Password Hashing Service/IPasswordHashingService.cs @@ -0,0 +1,8 @@ +namespace Services.PasswordHashing; + +public interface IPasswordHashingService +{ + byte[] Hash(string password); + + bool Verify(byte[] hash, string password); +} diff --git a/backend/user-service/user-service/src/Services/Password Hashing Service/PasswordHashingService.cs b/backend/user-service/user-service/src/Services/Password Hashing Service/PasswordHashingService.cs new file mode 100644 index 0000000..77c49c0 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Password Hashing Service/PasswordHashingService.cs @@ -0,0 +1,22 @@ +using Isopoh.Cryptography.Argon2; + +using Services.PasswordHashing; + +using System.Text; + +namespace Services.Auth; + +public class PasswordHashingService : IPasswordHashingService +{ + public byte[] Hash(string password) + { + string hashString = Argon2.Hash(password); + return Encoding.UTF8.GetBytes(hashString); + } + + public bool Verify(byte[] hash, string password) + { + string hashString = Encoding.UTF8.GetString(hash); + return Argon2.Verify(hashString, password); + } +} diff --git a/backend/user-service/user-service/src/Services/Token/ITokenPacketProcessorService.cs b/backend/user-service/user-service/src/Services/Token/ITokenPacketProcessorService.cs new file mode 100644 index 0000000..b0a7b1a --- /dev/null +++ b/backend/user-service/user-service/src/Services/Token/ITokenPacketProcessorService.cs @@ -0,0 +1,6 @@ +namespace Services.Token; + +public interface ITokenPacketProcessorService +{ + bool TryValidateToken(HttpRequest request, out Guid userId); +} \ No newline at end of file diff --git a/backend/user-service/user-service/src/Services/Token/TokenPacketProcessorService.cs b/backend/user-service/user-service/src/Services/Token/TokenPacketProcessorService.cs new file mode 100644 index 0000000..30f0aa3 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Token/TokenPacketProcessorService.cs @@ -0,0 +1,59 @@ +using Microsoft.IdentityModel.Tokens; + +using System.IdentityModel.Tokens.Jwt; + +using System.Security.Claims; +using System.Security.Cryptography; + +namespace Services.Token; + +public class TokenPacketProcessorService : ITokenPacketProcessorService +{ + private readonly TokenValidationParameters _validationParameters; + + public TokenPacketProcessorService() + { + var publicKeyPath = "public.pem"; + var publicKeyText = File.ReadAllText(publicKeyPath); + + var rsa = RSA.Create(); + rsa.ImportFromPem(publicKeyText.ToCharArray()); + + _validationParameters = new TokenValidationParameters + { + ValidateIssuer = false, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new RsaSecurityKey(rsa) + }; + } + + public bool TryValidateToken(HttpRequest request, out Guid userId) + { + userId = Guid.Empty; + + if (!request.Headers.TryGetValue("Authorization", out var authHeader)) + return false; + + var token = authHeader.ToString().Replace("Bearer ", ""); + + var handler = new JwtSecurityTokenHandler(); + + try + { + var principal = handler.ValidateToken(token, _validationParameters, out var _); + var userIdClaim = principal.FindFirst(ClaimTypes.NameIdentifier); + + if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var parsedId)) + { + userId = parsedId; + return true; + } + } + catch + { } + + return false; + } +} diff --git a/backend/user-service/user-service/src/Services/Users/AddUserService.cs b/backend/user-service/user-service/src/Services/Users/AddUserService.cs new file mode 100644 index 0000000..91b7386 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Users/AddUserService.cs @@ -0,0 +1,78 @@ +using Services.Database; +using Services.Database.Helpers; + +using Models.Users; + +namespace Services.Users; + +public class AddUserService +{ + private readonly IDbService _db; + + public AddUserService(IDbService db) + { + _db = db; + } + + public bool EmailExists(string email) + { + const string sql = "SELECT EXISTS(SELECT 1 FROM users WHERE email = @Email);"; + + return _db.QuerySingle(sql, reader => + reader.GetBoolean(0), + new { Email = email } + ); + } + + public Guid CreateUser(string fullName, string email, byte[] passwordHash) + { + const string sql = """ + INSERT INTO users (id, full_name, email, password) + VALUES (@Id, @FullName, @Email, @Password); + """; + + var id = Guid.NewGuid(); + + _db.Execute(sql, new + { + Id = id, + FullName = fullName, + Email = email, + Password = passwordHash + }); + + return id; + } + + public UserDto? GetById(Guid id) + { + const string sql = """ + SELECT + id, created_at, updated_at, last_login_at, is_active, + password, role_id, verification_status, organization_id, + email, full_name, phone, avatar_url, gov_id + FROM users + WHERE id = @Id + """; + + return _db.QuerySingle(sql, r => + { + return new UserDto( + r._GetGuid("id"), + r._GetDateTime("created_at"), + r._GetDateTime("updated_at"), + r._GetNullableDateTime("last_login_at"), + r._GetBool("is_active"), + Convert.ToBase64String(r._GetByteArray("password")), + r._GetGuid("role_id"), + r._GetString("verification_status"), + r._GetGuid("organization_id"), + r._GetString("email"), + r._GetString("full_name"), + r._GetString("phone"), + r._GetString("avatar_url"), + r._GetString("gov_id") + ); + }, new { Id = id }); + } +} \ No newline at end of file diff --git a/backend/user-service/user-service/src/Services/Users/ChangeUserService.cs b/backend/user-service/user-service/src/Services/Users/ChangeUserService.cs new file mode 100644 index 0000000..1e3ade0 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Users/ChangeUserService.cs @@ -0,0 +1,40 @@ +using Services.Database; + +using Models.Users; + +namespace Services.Users; + +public class ChangeUserService +{ + private readonly IDbService _db; + + public ChangeUserService(IDbService db) + { + _db = db; + } + + public void ChangeUser(Guid id, DateTime createdAt, string email, string fullName, string phone, string verificationStatus) + { + const string sql = """ + UPDATE users + SET + created_at = @CreatedAt, + email = @Email, + full_name = @FullName, + phone = @Phone, + verification_status = @VerificationStatus::verification_status_enum, + updated_at = NOW() + WHERE id = @Id + """; + + _db.Execute(sql, new + { + Id = id, + CreatedAt = createdAt, + Email = email, + FullName = fullName, + Phone = phone, + VerificationStatus = verificationStatus + }); + } +} \ No newline at end of file diff --git a/backend/user-service/user-service/src/Services/Users/GetUserService.cs b/backend/user-service/user-service/src/Services/Users/GetUserService.cs new file mode 100644 index 0000000..670fe72 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Users/GetUserService.cs @@ -0,0 +1,48 @@ +using Services.Database; +using Services.Database.Helpers; + +using Models.Users; + +namespace Services.Users; + +public class GetUserService +{ + private readonly IDbService _db; + + public GetUserService(IDbService db) + { + _db = db; + } + + public UserDto? GetUser(Guid id) + { + const string sql = """ + SELECT + id, created_at, updated_at, last_login_at, is_active, + password, role_id, verification_status, organization_id, + email, full_name, phone, avatar_url, gov_id + FROM users + WHERE id = @Id + """; + + return _db.QuerySingle(sql, r => + { + return new UserDto( + r._GetGuid("id"), + r._GetDateTime("created_at"), + r._GetDateTime("updated_at"), + r._GetNullableDateTime("last_login_at"), + r._GetBool("is_active"), + Convert.ToBase64String(r._GetByteArray("password")), + r._GetGuid("role_id"), + r._GetString("verification_status"), + r._GetGuid("organization_id"), + r._GetString("email"), + r._GetString("full_name"), + r._GetString("phone"), + r._GetString("avatar_url"), + r._GetString("gov_id") + ); + }, new { Id = id }); + } +} diff --git a/backend/user-service/user-service/src/Services/Users/GetUsersService.cs b/backend/user-service/user-service/src/Services/Users/GetUsersService.cs new file mode 100644 index 0000000..7f38917 --- /dev/null +++ b/backend/user-service/user-service/src/Services/Users/GetUsersService.cs @@ -0,0 +1,47 @@ +using Services.Database; +using Services.Database.Helpers; + +using Models.Users; + +namespace Services.Users; + +public class GetUsersService +{ + private readonly IDbService _db; + + public GetUsersService(IDbService db) + { + _db = db; + } + + public IEnumerable GetAllUsers() + { + const string sql = """ + SELECT + id, created_at, updated_at, last_login_at, is_active, + password, role_id, verification_status, organization_id, + email, full_name, phone, avatar_url, gov_id + FROM users + """; + + return _db.Query(sql, r => + { + return new UserDto( + r._GetGuid("id"), + r._GetDateTime("created_at"), + r._GetDateTime("updated_at"), + r._GetNullableDateTime("last_login_at"), + r._GetBool("is_active"), + Convert.ToBase64String(r._GetByteArray("password")), + r._GetGuid("role_id"), + r._GetString("verification_status"), + r._GetGuid("organization_id"), + r._GetString("email"), + r._GetString("full_name"), + r._GetString("phone"), + r._GetString("avatar_url"), + r._GetString("gov_id") + ); + }); + } +} diff --git a/backend/user-service/user-service/src/app.csproj b/backend/user-service/user-service/src/app.csproj new file mode 100644 index 0000000..6b75ec9 --- /dev/null +++ b/backend/user-service/user-service/src/app.csproj @@ -0,0 +1,15 @@ + + + + net8.0 + enable + enable + + + + + + + + + \ No newline at end of file diff --git a/backend/user-service/user-service/src/appsettings.json b/backend/user-service/user-service/src/appsettings.json new file mode 100644 index 0000000..6b26a17 --- /dev/null +++ b/backend/user-service/user-service/src/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "Postgres": "Host=192.168.1.174;Port=5432;Database=geo_db;Username=postgres;Password=sUpers3cRet" + } +} diff --git a/backend/user-service/user-service/user-service.csproj b/backend/user-service/user-service/user-service.csproj deleted file mode 100644 index 1b28a01..0000000 --- a/backend/user-service/user-service/user-service.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - -