Compare commits
2
Commits
eee0d44c92
...
25929286c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25929286c6 | ||
|
|
eae9f04b96 |
@@ -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 }}
|
||||||
@@ -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
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -17,6 +18,9 @@ public class Program
|
|||||||
.AddJsonFile("appsettings.json", false)
|
.AddJsonFile("appsettings.json", false)
|
||||||
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user