2 Commits
Author SHA1 Message Date
Dmitrii 25929286c6 Add AgreementPage and EncryptionHelper for transaction handling
Java CI / build (push) Successful in 48s
.NET+Docker CI/CD / Unit and Integration tests (push) Failing after 24s
.NET+Docker CI/CD / Push Docker image to ghcr.io (push) Has been skipped
Signed-off-by: Dmitrii <computer@yawaflua.tech>

Took 24 seconds
2026-06-29 03:01:17 +03:00
Dmitrii eae9f04b96 Refactor .gitignore and enhance transaction handling with encryption support
Signed-off-by: Dmitrii <computer@yawaflua.tech>

Took 48 minutes
2026-06-29 03:00:53 +03:00
10 changed files with 277 additions and 22 deletions
+69
View File
@@ -0,0 +1,69 @@
name: .NET+Docker CI/CD
on:
push:
branches: [ "master", "develop", "main" ]
paths-ignore:
- 'README.md'
- '*.md'
jobs:
build:
name: Unit and Integration tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
compose:
name: Push Docker image to ghcr.io
runs-on: ubuntu-latest
needs: build
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: yawaflua
password: ${{ env.GITHUB_TOKEN }}
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.yawaflua.tech
username: yawaflua
password: ${{ env.GITEA_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: |
ghcr.io/yawaflua/spmega
git.yawaflua.tech/yawaflua/spmega
- name: Build and push API
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: backend/
file: backend/SpMega.Backend/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+5
View File
@@ -19,3 +19,8 @@ hs_err_pid*
# Common working directory # Common working directory
run run
backend/SpMega.Backend/src/*
backend/SpMega.Backend/obj/*
backend/SpMega.Backend/appsettings.json
backend/SpMega.Backend/SpMega.Backend.http
-4
View File
@@ -1,4 +0,0 @@
SpMega.Backend/src/*
SpMega.Backend/obj/*
SpMega.Backend/appsettings.json
SpMega.Backend/SpMega.Backend.http
@@ -112,8 +112,9 @@ public class AuthController(AppDbContext dbContext, TokenService tokenService, I
Console.WriteLine(resp); Console.WriteLine(resp);
var user = ((User)HttpContext.Items["@me"]); var user = ((User)HttpContext.Items["@me"]);
Console.WriteLine(me.id); Console.WriteLine(me.id);
Console.WriteLine(Guid.Parse(me.minecraftUUID));
Console.WriteLine(user.Id.ToString()); Console.WriteLine(user.Id.ToString());
if (user == null || user.Id.ToString() != me.minecraftUUID) if (user == null || user.Id != Guid.Parse(me.minecraftUUID))
{ {
throw new Exception("Its not ur card"); throw new Exception("Its not ur card");
} }
@@ -1,6 +1,7 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -15,7 +16,7 @@ namespace SpMega.Backend.Controllers.v1;
[ApiController] [ApiController]
public class TransactionsController(AppDbContext context, ILogger<TransactionsController> logger, IConfiguration config) : ControllerBase public class TransactionsController(AppDbContext context, ILogger<TransactionsController> logger, IConfiguration config) : ControllerBase
{ {
private const string BASE_URL = "https://spworlds.ru/api/public"; private const string BASE_URL = "https://spworlds.ru/api/public/";
[HttpGet("{billId}")] [HttpGet("{billId}")]
public async Task<IActionResult> GetBill(string billId) public async Task<IActionResult> GetBill(string billId)
@@ -55,32 +56,59 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
[Authorize] [Authorize]
public async Task<ActionResult<Transaction>> CreateTransaction([FromBody] CreateTransactionBody body) public async Task<ActionResult<Transaction>> CreateTransaction([FromBody] CreateTransactionBody body)
{ {
var BearerToken = $"{body.cardToUse.Id}:{body.cardToUse.Token}"; var user = HttpContext.Items["@me"] as User;
string Base64BearerToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(BearerToken)); if (user == null)
{
return Unauthorized(new { error = "User not authenticated" });
}
var cardToUse = user.Cards.FirstOrDefault(l => l.Id == Guid.Parse(body.cardId));
if (cardToUse == null)
{
return BadRequest(new { error = "Card not found" });
}
var shortId = Program.GenerateRandomString(8); var shortId = Program.GenerateRandomString(5);
while (true)
{
var a = await context.Transactions.FirstOrDefaultAsync(k => k.ShortId == shortId);
if (a != null)
{
shortId = Program.GenerateRandomString(5);
}
else
{
break;
}
}
var transaction = new Transaction var transaction = new Transaction
{ {
ReceiverName = body.receiverName, ReceiverName = body.receiverName,
ShortId = shortId, ShortId = shortId,
ReceiverCardNumber = body.receiverCard, ReceiverCardNumber = body.receiverCard,
Sender = HttpContext.Items["@me"] as User, Sender = user,
SenderCardNumber = body.cardToUse.SpworldsID, SenderCardNumber = body.cardId,
Amount = body.amount, Amount = body.amount,
Comment = body.comment, Comment = body.comment,
}; };
try try
{ {
var uri = new Uri(new Uri(config["Url"] ?? "https://spmega.yawaflua.tech"), "/" + shortId); var uri = "s.ywfl.dev" + "/" + shortId;
var transitionInfo = new Dictionary<string, object> var transitionInfo = new Dictionary<string, object>
{ {
{ "receiver", body.receiverCard }, { "receiver", body.receiverCard },
{ "amount", body.amount }, { "amount", body.amount },
{ "comment", "Чек: "+ uri } { "comment", body.comment + ";Чек:"+ uri }
}; };
Console.WriteLine((body.comment + ";Чек: "+ uri).Length);
await SendRequest(endpoint: "transactions", body: transitionInfo, AuthHeader:new("Bearer", Base64BearerToken)); Console.WriteLine((body.comment + ";Чек: "+ uri));
var resp = await SendRequest(endpoint: "transactions", body: transitionInfo,
AuthHeader: new("Bearer", cardToUse.Token));
var balance = (int?)JsonNode.Parse(resp)?["balance"];
if (balance == null)
{
throw new Exception("Failed to create transaction: " + resp);
}
} catch (Exception exception) } catch (Exception exception)
{ {
@@ -104,6 +132,7 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
return Ok(await context.Transactions.Where(k => k.Sender.Id == ((User)HttpContext.Items["@me"]).Id).ToListAsync()); return Ok(await context.Transactions.Where(k => k.Sender.Id == ((User)HttpContext.Items["@me"]).Id).ToListAsync());
} }
[NonAction]
internal async Task<string> SendRequest(string endpoint, AuthenticationHeaderValue AuthHeader, HttpMethod method = null, object body = null) internal async Task<string> SendRequest(string endpoint, AuthenticationHeaderValue AuthHeader, HttpMethod method = null, object body = null)
{ {
method ??= body == null ? HttpMethod.Get : HttpMethod.Post; method ??= body == null ? HttpMethod.Get : HttpMethod.Post;
@@ -129,4 +158,4 @@ public class TransactionsController(AppDbContext context, ILogger<TransactionsCo
return await message.Content.ReadAsStringAsync(); return await message.Content.ReadAsStringAsync();
} }
} }
public record CreateTransactionBody(Card cardToUse, string receiverCard, string receiverName, string comment, int amount); public record CreateTransactionBody(string cardId, string receiverCard, string receiverName, string comment, int amount);
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace SpMega.Backend;
public class AgreementPage : PageModel
{
public void OnGet()
{
}
}
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using MongoDB.EntityFrameworkCore.Extensions; using MongoDB.EntityFrameworkCore.Extensions;
using SpMega.Backend.Persistent.Models.Transactions; using SpMega.Backend.Persistent.Models.Transactions;
using SpMega.Backend.Persistent.Models.Users; using SpMega.Backend.Persistent.Models.Users;
using SpMega.Backend.Services;
namespace SpMega.Backend.Persistent.Database; namespace SpMega.Backend.Persistent.Database;
@@ -20,6 +21,14 @@ public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(op
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>()
.OwnsMany(u => u.Cards, card =>
{
card.Property(c => c.Token)
.HasConversion(
v => EncryptionHelper.Encrypt(v),
v => EncryptionHelper.Decrypt(v)
);
});
} }
} }
@@ -2,14 +2,32 @@ using SPWorldsApi.Types.Interfaces;
namespace SpMega.Backend.Persistent.Models.DTO; namespace SpMega.Backend.Persistent.Models.DTO;
public class UserAccountDTO : IUserAccount public class UserAccountDTO
{ {
public string id { get; set; } public string id { get; set; }
public string username { get; set; } public string username { get; set; }
public string minecraftUUID { get; set; } public string minecraftUUID { get; set; }
public string status { get; set; } public string status { get; set; }
public List<string> roles { get; set; } public List<string> roles { get; set; }
public ICity city { get; set; } public CityDTO city { get; set; }
public List<IUserCard> cards { get; set; } public List<UserCardDTO> cards { get; set; }
public DateTime createdAt { get; set; } public DateTime createdAt { get; set; }
} }
public class UserCardDTO
{
public string id { get; set; }
public string name { get; set; }
public string number { get; set; }
public int color { get; set; }
}
public class CityDTO
{
public string id { get; set; }
public string name { get; set; }
public string description { get; set; }
public int x { get; set; }
public int z { get; set; }
public bool isMayor { get; set; }
}
+5 -2
View File
@@ -1,3 +1,4 @@
using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -18,6 +19,9 @@ public class Program
.AddJsonFile("appsettings.Development.json", false) .AddJsonFile("appsettings.Development.json", false)
.AddEnvironmentVariables(); .AddEnvironmentVariables();
var encryptionKey = builder.Configuration["Encryption:Key"] ?? "a-default-fallback-only-for-dev-key-change-this!";
EncryptionHelper.Initialize(encryptionKey);
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
builder.Services.AddLogging(logging => builder.Services.AddLogging(logging =>
{ {
@@ -79,12 +83,11 @@ public class Program
{ {
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var random = new Random();
var result = new StringBuilder(length); var result = new StringBuilder(length);
for (var i = 0; i < length; i++) for (var i = 0; i < length; i++)
{ {
var index = random.Next(chars.Length); var index = RandomNumberGenerator.GetInt32(chars.Length);
result.Append(chars[index]); result.Append(chars[index]);
} }
@@ -0,0 +1,114 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace SpMega.Backend.Services;
public static class EncryptionHelper
{
private static byte[]? _key;
/// <summary>
/// Initializes the encryption key. The key will be derived using SHA-256 from the provided secret string
/// to ensure it is exactly 32 bytes (256 bits) long.
/// </summary>
public static void Initialize(string secretKey)
{
if (string.IsNullOrWhiteSpace(secretKey))
{
throw new ArgumentException("Encryption key cannot be null or empty.", nameof(secretKey));
}
_key = SHA256.HashData(Encoding.UTF8.GetBytes(secretKey));
}
/// <summary>
/// Encrypts the plain text using AES-256-CBC.
/// A unique Initialization Vector (IV) is generated for each encryption and stored alongside the ciphertext.
/// </summary>
public static string Encrypt(string plainText)
{
if (_key == null)
{
throw new InvalidOperationException("EncryptionHelper has not been initialized. Call Initialize() first.");
}
if (string.IsNullOrEmpty(plainText))
{
return plainText;
}
using var aes = Aes.Create();
aes.Key = _key;
aes.GenerateIV();
var iv = aes.IV;
using var encryptor = aes.CreateEncryptor(aes.Key, iv);
using var ms = new MemoryStream();
// Write the IV to the beginning of the stream so it is stored alongside the ciphertext
ms.Write(iv, 0, iv.Length);
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
using (var writer = new StreamWriter(cs, Encoding.UTF8))
{
writer.Write(plainText);
}
return Convert.ToBase64String(ms.ToArray());
}
/// <summary>
/// Decrypts the cipher text using AES-256-CBC.
/// Reconstructs the Initialization Vector (IV) from the beginning of the ciphertext.
/// Falls back to returning the input string if decryption fails (e.g., for legacy plaintext tokens).
/// </summary>
public static string Decrypt(string cipherText)
{
if (_key == null)
{
throw new InvalidOperationException("EncryptionHelper has not been initialized. Call Initialize() first.");
}
if (string.IsNullOrEmpty(cipherText))
{
return cipherText;
}
try
{
var fullCipher = Convert.FromBase64String(cipherText);
using var aes = Aes.Create();
aes.Key = _key;
var ivLength = aes.BlockSize / 8;
if (fullCipher.Length < ivLength)
{
// Too short to be an encrypted payload, return original
return cipherText;
}
var iv = new byte[ivLength];
var cipher = new byte[fullCipher.Length - ivLength];
Buffer.BlockCopy(fullCipher, 0, iv, 0, ivLength);
Buffer.BlockCopy(fullCipher, ivLength, cipher, 0, cipher.Length);
aes.IV = iv;
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
using var ms = new MemoryStream(cipher);
using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
using var reader = new StreamReader(cs, Encoding.UTF8);
return reader.ReadToEnd();
}
catch (Exception ex) when (ex is CryptographicException or FormatException)
{
// Fallback for legacy unencrypted tokens
return cipherText;
}
}
}