Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25929286c6 | ||
|
|
eae9f04b96 | ||
|
|
eee0d44c92 | ||
|
|
7862597f45 | ||
|
|
eea62f90e2 |
@@ -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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
**/.dockerignore
|
||||||
|
**/.env
|
||||||
|
**/.git
|
||||||
|
**/.gitignore
|
||||||
|
**/.project
|
||||||
|
**/.settings
|
||||||
|
**/.toolstarget
|
||||||
|
**/.vs
|
||||||
|
**/.vscode
|
||||||
|
**/.idea
|
||||||
|
**/*.*proj.user
|
||||||
|
**/*.dbmdl
|
||||||
|
**/*.jfm
|
||||||
|
**/azds.yaml
|
||||||
|
**/bin
|
||||||
|
**/charts
|
||||||
|
**/docker-compose*
|
||||||
|
**/Dockerfile*
|
||||||
|
**/node_modules
|
||||||
|
**/npm-debug.log
|
||||||
|
**/obj
|
||||||
|
**/secrets.dev.yaml
|
||||||
|
**/values.dev.yaml
|
||||||
|
LICENSE
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using SpMega.Backend.Persistent.Database;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Authetication;
|
||||||
|
|
||||||
|
public class JwtAuthHandler(
|
||||||
|
IOptionsMonitor<JwtAuthenticationSchemeOptions> options,
|
||||||
|
ILoggerFactory logger,
|
||||||
|
UrlEncoder encoder,
|
||||||
|
AppDbContext dbContext) : AuthenticationHandler<JwtAuthenticationSchemeOptions>(options, logger, encoder)
|
||||||
|
{
|
||||||
|
|
||||||
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
|
{
|
||||||
|
if (!Request.Headers.ContainsKey("Authorization") || !Request.Headers.Authorization.ToString().StartsWith("Bearer "))
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
|
||||||
|
var token = Request.Headers.Authorization.ToString().Replace("Bearer ", "");
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(token))
|
||||||
|
{
|
||||||
|
return AuthenticateResult.Fail("Invalid authorization header format");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = await dbContext.Users.FirstOrDefaultAsync(k => k.Token == token);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
return AuthenticateResult.Fail("Invalid or expired token");
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||||
|
new(ClaimTypes.Name, user.Username),
|
||||||
|
new("UserId", user.Id.ToString()),
|
||||||
|
new("Token", token),
|
||||||
|
new(ClaimTypes.Role, "default")
|
||||||
|
};
|
||||||
|
|
||||||
|
var claimsIdentity = new ClaimsIdentity(claims, Scheme.Name);
|
||||||
|
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
|
||||||
|
|
||||||
|
Context.Items["User"] = user;
|
||||||
|
Context.Items["@me"] = user;
|
||||||
|
Context.Items["user"] = user;
|
||||||
|
|
||||||
|
var ticket = new AuthenticationTicket(claimsPrincipal, Scheme.Name);
|
||||||
|
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Error during JWT authentication");
|
||||||
|
return AuthenticateResult.Fail("Authentication error occurred");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Authetication;
|
||||||
|
|
||||||
|
public class JwtAuthenticationSchemeOptions : AuthenticationSchemeOptions
|
||||||
|
{
|
||||||
|
public const string DefaultScheme = "JwtScheme";
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using SpMega.Backend.Persistent.Database;
|
||||||
|
using SpMega.Backend.Persistent.Models.DTO;
|
||||||
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
using SpMega.Backend.Services;
|
||||||
|
using SPWorldsApi;
|
||||||
|
using SPWorldsApi.Types.Interfaces;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Controllers.v1;
|
||||||
|
|
||||||
|
|
||||||
|
public record GetSessionIdBody(string userName, Guid userUUID);
|
||||||
|
public record ValidateSessionBody(string sessionId, Guid userUUID);
|
||||||
|
[Route("api/v1/auth")]
|
||||||
|
[ApiController]
|
||||||
|
public class AuthController(AppDbContext dbContext, TokenService tokenService, ILogger<AuthController> logger) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string BASE_URL = "https://spworlds.ru/api/public";
|
||||||
|
public AuthenticationHeaderValue? AuthHeader { get; set; }
|
||||||
|
|
||||||
|
[HttpPost("start")]
|
||||||
|
public async Task<IActionResult> GetSessionIdAsync([FromBody] GetSessionIdBody body)
|
||||||
|
{
|
||||||
|
var userSession = new UserSession
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Token = Program.GenerateRandomString(15),
|
||||||
|
UserId = body.userUUID,
|
||||||
|
UserName = body.userName,
|
||||||
|
CreatedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
await dbContext.UserSessions.AddAsync(userSession);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
|
return Ok(new { sessionId = userSession.Token});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("validate")]
|
||||||
|
public async Task<ActionResult<User>> ValidateSessionAsync([FromBody]ValidateSessionBody body)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var session =
|
||||||
|
await dbContext.UserSessions.FirstOrDefaultAsync(k => k.UserId == body.userUUID && k.Token == body.sessionId);
|
||||||
|
if (session == null || session.CreatedAt.AddMinutes(2) < DateTime.UtcNow || session.IsDeleted)
|
||||||
|
throw new Exception("Session expired or not fount");
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
using var request = new HttpRequestMessage(
|
||||||
|
HttpMethod.Get,
|
||||||
|
$"https://sessionserver.mojang.com/session/minecraft/hasJoined?username={session.UserName}&serverId={session.Token}"
|
||||||
|
);
|
||||||
|
session.IsDeleted = true;
|
||||||
|
session.DeletedAt = DateTime.UtcNow;
|
||||||
|
session.UpdatedAt = DateTime.UtcNow;
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
var resp = await httpClient.SendAsync(request);
|
||||||
|
if (resp.StatusCode != HttpStatusCode.OK) throw new Exception("Mojang response is not OK");
|
||||||
|
Console.WriteLine(await resp.Content.ReadAsStringAsync());
|
||||||
|
var dto = await resp.Content.ReadFromJsonAsync<MojangDto>();
|
||||||
|
if (dto == null || dto.Name != session.UserName || Guid.Parse(dto.Id) != session.UserId) throw new Exception("Session expired, or dto is not acceptable.");
|
||||||
|
var token = tokenService.GenerateAccessToken(session.UserName, body.userUUID);
|
||||||
|
var user = await dbContext.Users.FirstOrDefaultAsync(k => k.Id == session.UserId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.Token = token;
|
||||||
|
dbContext.Update(user);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
user = new User
|
||||||
|
{
|
||||||
|
Id = body.userUUID,
|
||||||
|
Username = session.UserName,
|
||||||
|
Token = token,
|
||||||
|
};
|
||||||
|
await dbContext.AddAsync(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(user);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Error when validation session.");
|
||||||
|
return BadRequest("I think ure try to fool us. Try again later");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut("cards")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> AddCardAsync([FromBody] AddCardBody body)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var BearerToken = $"{body.id}:{body.token}";
|
||||||
|
string Base64BearerToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(BearerToken));
|
||||||
|
var resp = await SendRequest("/accounts/me", new("Bearer", Base64BearerToken));
|
||||||
|
var me = JsonSerializer.Deserialize<UserAccountDTO>(resp);
|
||||||
|
Console.WriteLine(resp);
|
||||||
|
var user = ((User)HttpContext.Items["@me"]);
|
||||||
|
Console.WriteLine(me.id);
|
||||||
|
Console.WriteLine(Guid.Parse(me.minecraftUUID));
|
||||||
|
Console.WriteLine(user.Id.ToString());
|
||||||
|
if (user == null || user.Id != Guid.Parse(me.minecraftUUID))
|
||||||
|
{
|
||||||
|
throw new Exception("Its not ur card");
|
||||||
|
}
|
||||||
|
|
||||||
|
var card = me.cards.First(k => k.id == body.id);
|
||||||
|
var existingCard = user.Cards.FirstOrDefault(k => k.Id.ToString() == card.id);
|
||||||
|
if (existingCard == null)
|
||||||
|
user.Cards.Add(new Card
|
||||||
|
{
|
||||||
|
Id = Guid.Parse(card.id),
|
||||||
|
Name = card.name,
|
||||||
|
SpworldsID = card.number,
|
||||||
|
Token = Base64BearerToken,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
else
|
||||||
|
{
|
||||||
|
|
||||||
|
existingCard.Name = card.name;
|
||||||
|
existingCard.SpworldsID = card.number;
|
||||||
|
existingCard.Token = Base64BearerToken;
|
||||||
|
existingCard.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
}
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Error when adding card.");
|
||||||
|
return BadRequest("Error when adding card.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet("cards")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<Card>>> GetAllCardsAsync()
|
||||||
|
{
|
||||||
|
return Ok(((User)HttpContext.Items["@me"]).Cards.Where(k => !k.IsDeleted).ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("cards/{id}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<IActionResult> DeleteCardAsync(Guid id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var user = (HttpContext.Items["@me"] as User);
|
||||||
|
user.Cards.RemoveAll(c => c.Id == id);
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
return Ok();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.LogError(e, "Error when deleting card.");
|
||||||
|
return BadRequest("Error when deleting card.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<string> SendRequest(string endpoint, AuthenticationHeaderValue AuthHeader, HttpMethod method = null, object body = null)
|
||||||
|
{
|
||||||
|
method ??= body == null ? HttpMethod.Get : HttpMethod.Post;
|
||||||
|
|
||||||
|
HttpResponseMessage message;
|
||||||
|
var client = new System.Net.Http.HttpClient();
|
||||||
|
|
||||||
|
using (var requestMessage = new HttpRequestMessage(method, BASE_URL + endpoint))
|
||||||
|
{
|
||||||
|
requestMessage.Content = new StringContent(
|
||||||
|
JsonSerializer.Serialize(body),
|
||||||
|
Encoding.UTF8, "application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
requestMessage.Headers.Authorization = AuthHeader;
|
||||||
|
|
||||||
|
message = await client.SendAsync(requestMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
return await message.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record AddCardBody(string id, string token);
|
||||||
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SpMega.Backend.Persistent.Database;
|
||||||
|
using SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
using SPWorldsApi;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Controllers.v1;
|
||||||
|
|
||||||
|
[Route("api/v1/transactions")]
|
||||||
|
[ApiController]
|
||||||
|
public class TransactionsController(AppDbContext context, ILogger<TransactionsController> logger, IConfiguration config) : ControllerBase
|
||||||
|
{
|
||||||
|
private const string BASE_URL = "https://spworlds.ru/api/public/";
|
||||||
|
|
||||||
|
[HttpGet("{billId}")]
|
||||||
|
public async Task<IActionResult> GetBill(string billId)
|
||||||
|
{
|
||||||
|
var transaction = await context.Transactions
|
||||||
|
.FirstOrDefaultAsync(t => t.ShortId == billId);
|
||||||
|
|
||||||
|
if (transaction == null)
|
||||||
|
{
|
||||||
|
if (billId == "00000000")
|
||||||
|
{
|
||||||
|
return Ok(new Transaction
|
||||||
|
{
|
||||||
|
Id = Guid.Empty,
|
||||||
|
ShortId = "00000000",
|
||||||
|
ReceiverName = "yawaflua",
|
||||||
|
ReceiverCardNumber = "00000",
|
||||||
|
Sender = new User
|
||||||
|
{
|
||||||
|
Id = default,
|
||||||
|
Username = "yawaflua",
|
||||||
|
Token = "123",
|
||||||
|
},
|
||||||
|
SenderCardNumber = "010101",
|
||||||
|
Amount = 42,
|
||||||
|
Comment = "API FIRST",
|
||||||
|
TransactionDate = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return NotFound(new { error = "Transaction not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut()]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<Transaction>> CreateTransaction([FromBody] CreateTransactionBody body)
|
||||||
|
{
|
||||||
|
var user = HttpContext.Items["@me"] as User;
|
||||||
|
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(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
|
||||||
|
{
|
||||||
|
ReceiverName = body.receiverName,
|
||||||
|
ShortId = shortId,
|
||||||
|
ReceiverCardNumber = body.receiverCard,
|
||||||
|
Sender = user,
|
||||||
|
SenderCardNumber = body.cardId,
|
||||||
|
Amount = body.amount,
|
||||||
|
Comment = body.comment,
|
||||||
|
};
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = "s.ywfl.dev" + "/" + shortId;
|
||||||
|
var transitionInfo = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "receiver", body.receiverCard },
|
||||||
|
{ "amount", body.amount },
|
||||||
|
{ "comment", body.comment + ";Чек:"+ uri }
|
||||||
|
};
|
||||||
|
Console.WriteLine((body.comment + ";Чек: "+ uri).Length);
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
logger.LogError(exception, "Error creating transaction");
|
||||||
|
return BadRequest(new { error = "Failed to create transaction" });
|
||||||
|
}
|
||||||
|
await context.AddAsync(transaction);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
return Ok(new Transaction
|
||||||
|
{
|
||||||
|
Id = transaction.Id,
|
||||||
|
ReceiverName = transaction.ReceiverName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet()]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<List<Transaction>>> GetAllTransactions()
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
method ??= body == null ? HttpMethod.Get : HttpMethod.Post;
|
||||||
|
|
||||||
|
HttpResponseMessage message;
|
||||||
|
var client = new System.Net.Http.HttpClient();
|
||||||
|
|
||||||
|
using (var requestMessage = new HttpRequestMessage(method, BASE_URL + endpoint))
|
||||||
|
{
|
||||||
|
requestMessage.Content = new StringContent(
|
||||||
|
JsonSerializer.Serialize(body),
|
||||||
|
Encoding.UTF8, "application/json"
|
||||||
|
);
|
||||||
|
|
||||||
|
requestMessage.Headers.Authorization = AuthHeader;
|
||||||
|
|
||||||
|
message = await client.SendAsync(requestMessage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Dispose();
|
||||||
|
|
||||||
|
return await message.Content.ReadAsStringAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public record CreateTransactionBody(string cardId, string receiverCard, string receiverName, string comment, int amount);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
|
||||||
|
USER $APP_UID
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
WORKDIR /src
|
||||||
|
COPY ["SpMega.Backend/SpMega.Backend.csproj", "SpMega.Backend/"]
|
||||||
|
RUN dotnet restore "SpMega.Backend/SpMega.Backend.csproj"
|
||||||
|
COPY . .
|
||||||
|
WORKDIR "/src/SpMega.Backend"
|
||||||
|
RUN dotnet build "./SpMega.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
|
FROM build AS publish
|
||||||
|
ARG BUILD_CONFIGURATION=Release
|
||||||
|
RUN dotnet publish "./SpMega.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
|
FROM base AS final
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=publish /app/publish .
|
||||||
|
ENTRYPOINT ["dotnet", "SpMega.Backend.dll"]
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
@page "/privacy"
|
||||||
|
@model SpMega.Backend.AgreementPage
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Пользовательское соглашение — SPMega</title>
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color: #0b0f17;
|
||||||
|
--surface-color: #121824;
|
||||||
|
--surface-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--primary: #3b82f6;
|
||||||
|
--primary-glow: rgba(59, 130, 246, 0.15);
|
||||||
|
--success: #10b981;
|
||||||
|
--success-glow: rgba(16, 185, 129, 0.15);
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--warning-glow: rgba(245, 158, 11, 0.1);
|
||||||
|
--text-main: #f3f4f6;
|
||||||
|
--text-muted: #9ca3af;
|
||||||
|
--text-link: #60a5fa;
|
||||||
|
--font-sans: 'Inter', sans-serif;
|
||||||
|
--font-display: 'Outfit', sans-serif;
|
||||||
|
--font-mono: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
min-height: 100vh;
|
||||||
|
line-height: 1.6;
|
||||||
|
overflow-x: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle glowing background blobs */
|
||||||
|
.glow-blob {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(120px);
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-1 {
|
||||||
|
width: 400px;
|
||||||
|
height: 400px;
|
||||||
|
background: rgba(59, 130, 246, 0.12);
|
||||||
|
top: -100px;
|
||||||
|
left: -100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blob-2 {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
background: rgba(16, 185, 129, 0.08);
|
||||||
|
bottom: -150px;
|
||||||
|
right: -100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: 1200px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 24px 80px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Header */
|
||||||
|
header {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
border-bottom: 1px solid var(--surface-border);
|
||||||
|
padding-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: linear-gradient(135deg, var(--primary), var(--success));
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 800;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 18px;
|
||||||
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
background: linear-gradient(to right, #ffffff, #9ca3af);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout Grid */
|
||||||
|
.layout-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky Sidebar Table of Contents */
|
||||||
|
.sidebar {
|
||||||
|
position: sticky;
|
||||||
|
top: 40px;
|
||||||
|
background: var(--surface-color);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 16px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-block;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-link:hover, .toc-link.active {
|
||||||
|
color: var(--primary);
|
||||||
|
border-left-color: var(--primary);
|
||||||
|
padding-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Legal Content */
|
||||||
|
.document-container {
|
||||||
|
background: var(--surface-color);
|
||||||
|
border: 1px solid var(--surface-border);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px 48px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
background: linear-gradient(135deg, #ffffff, #e5e7eb);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-box {
|
||||||
|
background: var(--warning-glow);
|
||||||
|
border-left: 4px solid var(--warning);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
margin-bottom: 36px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #fce7f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-box strong {
|
||||||
|
color: var(--warning);
|
||||||
|
font-family: var(--font-display);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
scroll-margin-top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section h2 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section h2::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 6px;
|
||||||
|
height: 20px;
|
||||||
|
background: var(--primary);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section p {
|
||||||
|
color: rgba(243, 244, 246, 0.85);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-size: 15px;
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section ul {
|
||||||
|
list-style: none;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14.5px;
|
||||||
|
color: rgba(243, 244, 246, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.doc-section li::before {
|
||||||
|
content: "✓";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--success);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer elements */
|
||||||
|
footer {
|
||||||
|
margin-top: auto;
|
||||||
|
border-top: 1px solid var(--surface-border);
|
||||||
|
padding: 24px 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scroll to top button */
|
||||||
|
.scroll-top {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 30px;
|
||||||
|
right: 30px;
|
||||||
|
background: var(--primary);
|
||||||
|
color: #ffffff;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-top.visible {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-top:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@@keyframes slideUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@@media (max-width: 960px) {
|
||||||
|
.layout-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
display: none; /* Hide sidebar table of contents on smaller screens */
|
||||||
|
}
|
||||||
|
.document-container {
|
||||||
|
padding: 30px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 600px) {
|
||||||
|
header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.meta-info {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.doc-title {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.doc-subtitle {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="glow-blob blob-1"></div>
|
||||||
|
<div class="glow-blob blob-2"></div>
|
||||||
|
|
||||||
|
<div class="wrapper">
|
||||||
|
<header>
|
||||||
|
<a href="#" class="logo-group">
|
||||||
|
<div class="logo-icon">S</div>
|
||||||
|
<div class="logo-text">SPMega</div>
|
||||||
|
</a>
|
||||||
|
<div class="meta-info">
|
||||||
|
<div>Версия: <span class="badge">1.0.0-2026</span></div>
|
||||||
|
<div style="margin-top: 4px;">Обновлено: 28 июня 2026</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="layout-grid">
|
||||||
|
<!-- Sidebar Navigation -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<h3 class="sidebar-title">Содержание</h3>
|
||||||
|
<ul class="toc-list">
|
||||||
|
<li><a href="#intro" class="toc-link active" id="link-intro">1. Общие положения</a></li>
|
||||||
|
<li><a href="#minecraft-mod" class="toc-link" id="link-minecraft-mod">2. Использование мода</a></li>
|
||||||
|
<li><a href="#disclaimer" class="toc-link" id="link-disclaimer">3. Отказ от претензий</a></li>
|
||||||
|
<li><a href="#data-collection" class="toc-link" id="link-data-collection">4. Сбор данных</a></li>
|
||||||
|
<li><a href="#privacy" class="toc-link" id="link-privacy">5. Конфиденциальность</a></li>
|
||||||
|
<li><a href="#changes" class="toc-link" id="link-changes">6. Изменение соглашения</a></li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Document Content -->
|
||||||
|
<div class="document-container">
|
||||||
|
<h1 class="doc-title">Пользовательское соглашение</h1>
|
||||||
|
<div class="doc-subtitle">
|
||||||
|
<span>Свод правил для пользователей модификации Minecraft и сервиса SPMega</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert-box">
|
||||||
|
<strong>Важная информация</strong>
|
||||||
|
Установка, запуск модификации SPMega для Minecraft или посещение данного сайта означает ваше полное согласие со всеми пунктами данного соглашения. Если вы не согласны с какими-либо условиями, вы обязаны прекратить использование всех связанных сервисов и удалить модификацию.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 1 -->
|
||||||
|
<section id="intro" class="doc-section">
|
||||||
|
<h2>Общие положения</h2>
|
||||||
|
<p>Настоящее Соглашение является публичной офертой и регулирует порядок использования официального программного обеспечения (модификации для Minecraft), а также веб-сервисов и API, предоставляемых под эгидой проекта SPMega.</p>
|
||||||
|
<p>Соглашение заключается между конечным пользователем (вами) и разработчиками проекта (автором модификации и владельцами сайта). Нажимая кнопку запуска в лаунчере, заходя в игру с активным модом или посещая данный ресурс, вы подтверждаете свою дееспособность и согласие с правилами.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 2 -->
|
||||||
|
<section id="minecraft-mod" class="doc-section">
|
||||||
|
<h2>Использование мода для Minecraft</h2>
|
||||||
|
<p>Модификация SPMega разработана для интеграции игрового процесса Minecraft с нашей платежной и статистической системой. Пользователю предоставляется бесплатное неисключительное право на использование модификации в личных некоммерческих целях.</p>
|
||||||
|
<p>При использовании модификации запрещено:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Декомпилировать, модифицировать код или создавать клоны ПО без прямого письменного согласия разработчиков.</li>
|
||||||
|
<li>Использовать баги мода или веб-интерфейсов для накрутки баланса, валюты или совершения несанкционированных транзакций.</li>
|
||||||
|
<li>Использовать модификацию в связке с запрещенными сторонними программами (чит-клиенты, боты и т.д.).</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 3 -->
|
||||||
|
<section id="disclaimer" class="doc-section">
|
||||||
|
<h2>Отказ от претензий (Дисклеймер)</h2>
|
||||||
|
<p><strong>ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ» (AS IS).</strong> Автор модификации и владельцы сайта снимают с себя всю ответственность за любой ущерб, возникший в результате использования мода или веб-ресурсов.</p>
|
||||||
|
<p>Пользователь осознает и соглашается, что:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Автор не несет ответственности за сбои в игровом клиенте Minecraft, потерю игрового прогресса, повреждение файлов сохранений или несовместимость с другими модификациями.</li>
|
||||||
|
<li>Владельцы сайта не гарантируют 100% бесперебойную доступность серверов API и ведения транзакций.</li>
|
||||||
|
<li>Пользователь добровольно отказывается от любых финансовых, юридических, моральных или иных материальных претензий к авторам и администрации в случае технических сбоев.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 4 -->
|
||||||
|
<section id="data-collection" class="doc-section">
|
||||||
|
<h2>Сбор данных и статистики</h2>
|
||||||
|
<p>Для корректного функционирования игровых транзакций, выписок, защиты от мошенничества и ведения честной игровой статистики, сайт и модификация SPMega осуществляют автоматический сбор технических параметров.</p>
|
||||||
|
<p>Мы собираем и обрабатываем:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Ваш уникальный идентификатор Minecraft (<span class="badge">UUID</span>) и текущий никнейм.</li>
|
||||||
|
<li>Сведения о версии игры, версии Java и используемой версии модификации.</li>
|
||||||
|
<li>Ваш текущий IP-адрес для предотвращения мультиаккаунтинга и выявления попыток взлома API.</li>
|
||||||
|
<li>Техническую информацию о транзакциях (суммы, комментарии, номера виртуальных счетов/карт и идентификаторы).</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 5 -->
|
||||||
|
<section id="privacy" class="doc-section">
|
||||||
|
<h2>Конфиденциальность и безопасность данных</h2>
|
||||||
|
<p>Мы высоко ценим доверие наших пользователей и гарантируем сохранность собираемой информации. Все личные данные передаются и обрабатываются по защищенным зашифрованным каналам связи (HTTPS/SSL).</p>
|
||||||
|
<p>Администрация сайта строго обязуется:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Не передавать, не продавать и не распространять</strong> ваши личные данные, IP-адреса и статистику третьим лицам.</li>
|
||||||
|
<li>Использовать собранную анонимизированную статистику исключительно во внутренних аналитических целях для улучшения производительности мода.</li>
|
||||||
|
<li>Раскрывать информацию только в случаях, прямо предусмотренных действующим законодательством при наличии официальных судебных или правоохранительных запросов.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section 6 -->
|
||||||
|
<section id="changes" class="doc-section">
|
||||||
|
<h2>Изменение условий соглашения</h2>
|
||||||
|
<p>Администрация оставляет за собой право в одностороннем порядке вносить изменения в настоящее Пользовательское соглашение без предварительного индивидуального уведомления пользователей.</p>
|
||||||
|
<p>Новая редакция Соглашения вступает в силу с момента ее публикации на данной веб-странице. Мы рекомендуем периодически проверять условия соглашения, чтобы оставаться в курсе актуальных правил использования.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="/v1/BillPage/00000000-0000-0000-0000-000000000000" class="footer-link">Проверить чек</a>
|
||||||
|
<span>•</span>
|
||||||
|
<a href="#" class="footer-link">Поддержка</a>
|
||||||
|
</div>
|
||||||
|
<div>© 2026 SPMega. Все права защищены. Minecraft является торговой маркой Mojang Synergies AB.</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<!-- Scroll to Top Button -->
|
||||||
|
<button class="scroll-top" id="scrollTopBtn" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
|
||||||
|
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 15.75l7.5-7.5 7.5 7.5" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Smooth scroll spy to highlight active sidebar links
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const sections = document.querySelectorAll('.doc-section');
|
||||||
|
const navLinks = document.querySelectorAll('.toc-link');
|
||||||
|
const scrollTopBtn = document.getElementById('scrollTopBtn');
|
||||||
|
|
||||||
|
function highlightNav() {
|
||||||
|
let current = '';
|
||||||
|
|
||||||
|
// Show/hide scroll top button
|
||||||
|
if (window.scrollY > 300) {
|
||||||
|
scrollTopBtn.classList.add('visible');
|
||||||
|
} else {
|
||||||
|
scrollTopBtn.classList.remove('visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll spy logic
|
||||||
|
sections.forEach(section => {
|
||||||
|
const sectionTop = section.offsetTop;
|
||||||
|
const sectionHeight = section.clientHeight;
|
||||||
|
if (window.scrollY >= sectionTop - 120) {
|
||||||
|
current = section.getAttribute('id');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navLinks.forEach(link => {
|
||||||
|
link.classList.remove('active');
|
||||||
|
if (link.getAttribute('href') === `#${current}`) {
|
||||||
|
link.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('scroll', highlightNav);
|
||||||
|
highlightNav(); // Trigger once on load
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace SpMega.Backend;
|
||||||
|
|
||||||
|
public class AgreementPage : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,688 @@
|
|||||||
|
@page "/{id?}"
|
||||||
|
@model SpMega.Backend.Pages.v1.BillPage
|
||||||
|
|
||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Чек транзакции — SPMega</title>
|
||||||
|
|
||||||
|
<!-- Google Fonts -->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-gradient: radial-gradient(circle at 50% 50%, #1a1e29 0%, #0d0f14 100%);
|
||||||
|
--paper-color: #faf9f6;
|
||||||
|
--text-dark: #1f2937;
|
||||||
|
--text-muted: #6b7280;
|
||||||
|
--primary: #10b981;
|
||||||
|
--primary-hover: #059669;
|
||||||
|
--error: #ef4444;
|
||||||
|
--border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: var(--bg-gradient);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #ffffff;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient glowing background */
|
||||||
|
.ambient-glow {
|
||||||
|
position: absolute;
|
||||||
|
width: 600px;
|
||||||
|
height: 600px;
|
||||||
|
background: radial-gradient(circle, rgba(16, 185, 129, 0.08) 0%, rgba(0,0,0,0) 70%);
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Receipt paper style */
|
||||||
|
.receipt-paper {
|
||||||
|
background-color: var(--paper-color);
|
||||||
|
color: var(--text-dark);
|
||||||
|
width: 100%;
|
||||||
|
padding: 40px 28px 30px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Torn paper effect using CSS linear gradients */
|
||||||
|
.receipt-paper::before, .receipt-paper::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 12px;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt-paper::before {
|
||||||
|
top: -12px;
|
||||||
|
background-image: linear-gradient(135deg, transparent 25%, var(--paper-color) 25%, var(--paper-color) 75%, transparent 75%),
|
||||||
|
linear-gradient(225deg, transparent 25%, var(--paper-color) 25%, var(--paper-color) 75%, transparent 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt-paper::after {
|
||||||
|
bottom: -12px;
|
||||||
|
background-image: linear-gradient(45deg, transparent 25%, var(--paper-color) 25%, var(--paper-color) 75%, transparent 75%),
|
||||||
|
linear-gradient(-45deg, transparent 25%, var(--paper-color) 25%, var(--paper-color) 75%, transparent 75%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stamp style */
|
||||||
|
.stamp {
|
||||||
|
position: absolute;
|
||||||
|
top: 22%;
|
||||||
|
right: 10%;
|
||||||
|
border: 3px double var(--primary);
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 4px 10px;
|
||||||
|
transform: rotate(-15deg);
|
||||||
|
opacity: 0.85;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: rgba(250, 249, 246, 0.8);
|
||||||
|
box-shadow: 0 0 0 2px var(--primary);
|
||||||
|
animation: stampIn 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.5s both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stamp.error {
|
||||||
|
border-color: var(--error);
|
||||||
|
color: var(--error);
|
||||||
|
box-shadow: 0 0 0 2px var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header elements */
|
||||||
|
.receipt-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt-logo {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 20px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: #111827;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receipt-subtitle {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
border-top: 2px dashed #d1d5db;
|
||||||
|
margin: 20px 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider::before, .divider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #0d0f14; /* cutouts on sides */
|
||||||
|
border-radius: 50%;
|
||||||
|
top: -6px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
.divider::before { left: -34px; }
|
||||||
|
.divider::after { right: -34px; }
|
||||||
|
|
||||||
|
/* Meta block */
|
||||||
|
.meta-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value {
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-value.bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Amount styling */
|
||||||
|
.amount-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-title {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-val {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #111827;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comments box */
|
||||||
|
.comment-box {
|
||||||
|
background: rgba(0, 0, 0, 0.03);
|
||||||
|
border-left: 3px solid #9ca3af;
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #374151;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer tools */
|
||||||
|
.receipt-footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-wrapper {
|
||||||
|
margin: 15px auto;
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-code-wrapper img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barcode {
|
||||||
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
|
margin: 15px auto 4px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#111827,
|
||||||
|
#111827 2px,
|
||||||
|
transparent 2px,
|
||||||
|
transparent 5px,
|
||||||
|
#111827 5px,
|
||||||
|
#111827 6px,
|
||||||
|
transparent 6px,
|
||||||
|
transparent 9px,
|
||||||
|
#111827 9px,
|
||||||
|
#111827 11px,
|
||||||
|
transparent 11px,
|
||||||
|
transparent 13px
|
||||||
|
);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.barcode-text {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action buttons */
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 30px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: var(--primary);
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
|
color: #e5e7eb;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.15);
|
||||||
|
color: #ffffff;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading / Error skeletons */
|
||||||
|
.loader-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 50px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 3px solid rgba(16, 185, 129, 0.1);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: var(--primary);
|
||||||
|
animation: spin 1s ease-in-out infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 40px;
|
||||||
|
color: var(--error);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast notification */
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 24px;
|
||||||
|
background: rgba(17, 24, 39, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transform: translateY(100px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(15px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes stampIn {
|
||||||
|
from { opacity: 0; transform: rotate(-25deg) scale(1.6); filter: blur(2px); }
|
||||||
|
to { opacity: 0.85; transform: rotate(-15deg) scale(1); filter: blur(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printable stylesheet */
|
||||||
|
@@media print {
|
||||||
|
body {
|
||||||
|
background: #ffffff !important;
|
||||||
|
color: #000000 !important;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.ambient-glow, .action-buttons, .toast {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.receipt-wrapper {
|
||||||
|
max-width: 100%;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
.receipt-paper {
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 10px;
|
||||||
|
border: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #ffffff !important;
|
||||||
|
}
|
||||||
|
.receipt-paper::before, .receipt-paper::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.divider::before, .divider::after {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.stamp {
|
||||||
|
background: none !important;
|
||||||
|
border-style: solid !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="ambient-glow"></div>
|
||||||
|
|
||||||
|
<div class="receipt-wrapper">
|
||||||
|
|
||||||
|
<div class="receipt-paper" id="receiptContent">
|
||||||
|
<!-- Dynamic Content Injected Here -->
|
||||||
|
<div class="loader-container" id="loadingState">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<div style="color: var(--text-muted)">Получение данных чека...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons" id="actionButtons" style="display: none;">
|
||||||
|
<button class="btn btn-secondary" onclick="copyReceiptLink()">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
||||||
|
</svg>
|
||||||
|
Ссылка
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" onclick="window.print()">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-3a2 2 0 00-2-2H9a2 2 0 00-2 2v3a2 2 0 002 2zm5-17v2m-6 4h12" />
|
||||||
|
</svg>
|
||||||
|
Печать чека
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Copy Toast -->
|
||||||
|
<div class="toast" id="copyToast">
|
||||||
|
<svg width="18" height="18" fill="none" stroke="var(--primary)" stroke-width="2.5" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<span>Ссылка скопирована в буфер обмена</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function getTransactionId() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const idParam = urlParams.get('billId') || urlParams.get('id') || urlParams.get('transactionId');
|
||||||
|
if (idParam) return idParam;
|
||||||
|
|
||||||
|
const pathSegments = window.location.pathname.split('/').filter(p => p);
|
||||||
|
if (pathSegments.length > 0) {
|
||||||
|
return pathSegments[pathSegments.length - 1];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(isoString) {
|
||||||
|
try {
|
||||||
|
const date = new Date(isoString);
|
||||||
|
if (isNaN(date.getTime())) return isoString;
|
||||||
|
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
const d = pad(date.getDate());
|
||||||
|
const m = pad(date.getMonth() + 1);
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const h = pad(date.getHours());
|
||||||
|
const min = pad(date.getMinutes());
|
||||||
|
const s = pad(date.getSeconds());
|
||||||
|
|
||||||
|
return `${d}.${m}.${y} ${h}:${min}:${s}`;
|
||||||
|
} catch (e) {
|
||||||
|
return isoString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadBill() {
|
||||||
|
const billId = getTransactionId();
|
||||||
|
const receiptContentEl = document.getElementById('receiptContent');
|
||||||
|
const actionButtonsEl = document.getElementById('actionButtons');
|
||||||
|
|
||||||
|
if (!billId) {
|
||||||
|
renderError('Идентификатор транзакции не указан. Пожалуйста, укажите ShortId в URL-адресе.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch transaction details from API
|
||||||
|
const response = await fetch(`/api/v1/transactions/${billId}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Ошибка сервера: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = await response.json();
|
||||||
|
|
||||||
|
// Success stamp and details rendering
|
||||||
|
const formattedDate = formatDate(transaction.transactionDate || new Date().toISOString());
|
||||||
|
const senderCard = (transaction.senderCardNumber);
|
||||||
|
const receiverCard = (transaction.receiverCardNumber);
|
||||||
|
|
||||||
|
let commentHtml = '';
|
||||||
|
if (transaction.comment && transaction.comment.trim() !== '') {
|
||||||
|
commentHtml = `
|
||||||
|
<div class="comment-box">
|
||||||
|
<strong>Назначение:</strong><br/>
|
||||||
|
${escapeHtml(transaction.comment)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make dynamic QR code
|
||||||
|
const qrUrl = encodeURIComponent(window.location.href);
|
||||||
|
const qrCodeSrc = `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${qrUrl}&color=111827&bgcolor=faf9f6`;
|
||||||
|
|
||||||
|
receiptContentEl.innerHTML = `
|
||||||
|
<!-- Inked stamp of completion -->
|
||||||
|
<div class="stamp">Оплачено</div>
|
||||||
|
|
||||||
|
<div class="receipt-header">
|
||||||
|
<div class="receipt-logo">SPMEGA PAY</div>
|
||||||
|
<div class="receipt-subtitle">Электронная квитанция</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="amount-section">
|
||||||
|
<div class="amount-title">Сумма платежа</div>
|
||||||
|
<div class="amount-val">${Number(transaction.amount)} АР</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">ID транзакции:</span>
|
||||||
|
<span class="meta-value" style="font-size: 11px;">${transaction.id}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">Дата & Время:</span>
|
||||||
|
<span class="meta-value">${formattedDate}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">Отправитель:</span>
|
||||||
|
<span class="meta-value bold">${escapeHtml(transaction.sender?.username || 'Клиент SPMega')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">Карта списания:</span>
|
||||||
|
<span class="meta-value">${senderCard}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider" style="margin: 10px 0; border-top: 1px dotted #9ca3af;"></div>
|
||||||
|
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">Получатель:</span>
|
||||||
|
<span class="meta-value bold">${escapeHtml(transaction.receiverName || 'Не указан')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta-row">
|
||||||
|
<span class="meta-label">Карта зачисления:</span>
|
||||||
|
<span class="meta-value">${receiverCard}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${commentHtml}
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="receipt-footer">
|
||||||
|
<div style="font-size: 10px; color: var(--text-muted); margin-bottom: 12px; text-transform: uppercase;">
|
||||||
|
Сканируйте для проверки
|
||||||
|
</div>
|
||||||
|
<div class="qr-code-wrapper">
|
||||||
|
<img src="${qrCodeSrc}" alt="QR-код чека" onerror="this.parentElement.style.display='none'" />
|
||||||
|
</div>
|
||||||
|
<div class="barcode"></div>
|
||||||
|
<div class="barcode-text">spmega*transaction*chk</div>
|
||||||
|
<div style="font-size: 10px; color: var(--text-muted); margin-top: 15px;">
|
||||||
|
Спасибо за то, что вы с нами!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Display operations buttons
|
||||||
|
actionButtonsEl.style.display = 'flex';
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
renderError('Не удалось загрузить чек. Убедитесь, что GUID транзакции верен и сервер доступен.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderError(message) {
|
||||||
|
const receiptContentEl = document.getElementById('receiptContent');
|
||||||
|
receiptContentEl.innerHTML = `
|
||||||
|
<div class="stamp error">Ошибка</div>
|
||||||
|
<div class="receipt-header">
|
||||||
|
<div class="receipt-logo">SPMEGA PAY</div>
|
||||||
|
<div class="receipt-subtitle">Проблема с транзакцией</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="error-state">
|
||||||
|
<div class="error-icon">⚠</div>
|
||||||
|
<div style="font-weight: 600; color: #111827; margin-bottom: 8px;">Чек не найден</div>
|
||||||
|
<div style="color: var(--text-muted); font-size: 12px; line-height: 1.5;">${escapeHtml(message)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<div class="receipt-footer">
|
||||||
|
<div class="barcode"></div>
|
||||||
|
<div class="barcode-text">error*code*404</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyReceiptLink() {
|
||||||
|
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||||
|
const toast = document.getElementById('copyToast');
|
||||||
|
toast.classList.add('show');
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.classList.remove('show');
|
||||||
|
}, 3000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Could not copy text: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize loading on DOM ready
|
||||||
|
window.addEventListener('DOMContentLoaded', loadBill);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Pages.v1;
|
||||||
|
|
||||||
|
public class BillPage : PageModel
|
||||||
|
{
|
||||||
|
public void OnGet()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MongoDB.EntityFrameworkCore.Extensions;
|
||||||
|
using SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
using SpMega.Backend.Services;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Persistent.Database;
|
||||||
|
|
||||||
|
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
internal DbSet<Transaction> Transactions { get; set; }
|
||||||
|
internal DbSet<User> Users { get; set; }
|
||||||
|
internal DbSet<UserSession> UserSessions { get; set; }
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder 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)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SpMega.Backend.Persistent.Models.DTO;
|
||||||
|
|
||||||
|
public class PropertyDto
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Signature { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MojangDto
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public List<PropertyDto> Properties { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using SPWorldsApi.Types.Interfaces;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Persistent.Models.DTO;
|
||||||
|
|
||||||
|
public class UserAccountDTO
|
||||||
|
{
|
||||||
|
public string id { get; set; }
|
||||||
|
public string username { get; set; }
|
||||||
|
public string minecraftUUID { get; set; }
|
||||||
|
public string status { get; set; }
|
||||||
|
public List<string> roles { get; set; }
|
||||||
|
public CityDTO city { get; set; }
|
||||||
|
public List<UserCardDTO> cards { 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; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
|
||||||
|
public class Transaction
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string ShortId { get; set; } = Program.GenerateRandomString(8);
|
||||||
|
public string ReceiverName { get; set; }
|
||||||
|
public string ReceiverCardNumber { get; set; }
|
||||||
|
[JsonIgnore] public User Sender { get; set; }
|
||||||
|
public string SenderCardNumber { get; set; }
|
||||||
|
|
||||||
|
public int Amount { get; set; } = 0;
|
||||||
|
public string Comment { get; set; } = "";
|
||||||
|
|
||||||
|
public DateTime TransactionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
|
[Owned]
|
||||||
|
public class Card
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string SpworldsID { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using SpMega.Backend.Persistent.Models.Transactions;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
public List<Card> Cards { get; set; } = [];
|
||||||
|
public List<Transaction> Transactions { get; set; } = [];
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace SpMega.Backend.Persistent.Models.Users;
|
||||||
|
|
||||||
|
public class UserSession
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public User? User { get; set; } = null;
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
public DateTime? DeletedAt { get; set; }
|
||||||
|
public bool IsDeleted { get; set; } = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MongoDB.Driver;
|
||||||
|
using SpMega.Backend.Authetication;
|
||||||
|
using SpMega.Backend.Persistent.Database;
|
||||||
|
using SpMega.Backend.Services;
|
||||||
|
|
||||||
|
namespace SpMega.Backend;
|
||||||
|
|
||||||
|
public class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
builder.Configuration
|
||||||
|
.AddJsonFile("appsettings.json", false)
|
||||||
|
.AddJsonFile("appsettings.Development.json", false)
|
||||||
|
.AddEnvironmentVariables();
|
||||||
|
|
||||||
|
var encryptionKey = builder.Configuration["Encryption:Key"] ?? "a-default-fallback-only-for-dev-key-change-this!";
|
||||||
|
EncryptionHelper.Initialize(encryptionKey);
|
||||||
|
|
||||||
|
builder.Services.AddAuthorization();
|
||||||
|
builder.Services.AddLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.AddConsole();
|
||||||
|
logging.AddDebug();
|
||||||
|
});
|
||||||
|
builder.Services
|
||||||
|
.AddScoped<TokenService>()
|
||||||
|
.AddRazorPages();
|
||||||
|
builder.Services
|
||||||
|
.AddHttpLogging(logging =>
|
||||||
|
{
|
||||||
|
logging.LoggingFields = Microsoft.AspNetCore.HttpLogging.HttpLoggingFields.All;
|
||||||
|
})
|
||||||
|
.AddRouting()
|
||||||
|
.AddControllers();
|
||||||
|
|
||||||
|
|
||||||
|
builder.Services.AddAuthentication(options =>
|
||||||
|
{
|
||||||
|
options.DefaultAuthenticateScheme = JwtAuthenticationSchemeOptions.DefaultScheme;
|
||||||
|
options.DefaultChallengeScheme = JwtAuthenticationSchemeOptions.DefaultScheme;
|
||||||
|
})
|
||||||
|
.AddScheme<JwtAuthenticationSchemeOptions, JwtAuthHandler>(
|
||||||
|
JwtAuthenticationSchemeOptions.DefaultScheme,
|
||||||
|
options => { });
|
||||||
|
|
||||||
|
builder.Services.AddAuthorizationBuilder()
|
||||||
|
.SetDefaultPolicy(new AuthorizationPolicyBuilder()
|
||||||
|
.RequireAuthenticatedUser()
|
||||||
|
.Build());
|
||||||
|
|
||||||
|
var serviceCollection = new ServiceCollection();
|
||||||
|
var mongoClient =
|
||||||
|
new MongoClient(builder.Configuration.GetValue<string>("Mongo") ?? "mongodb://curiosity:27018");
|
||||||
|
serviceCollection.AddSingleton<IMongoClient>(mongoClient);
|
||||||
|
|
||||||
|
builder.Services.AddDbContext<AppDbContext>((_, options) =>
|
||||||
|
{
|
||||||
|
options.UseMongoDB(mongoClient, "spmega");
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), appBuilder =>
|
||||||
|
{
|
||||||
|
appBuilder.UseHttpLogging();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.MapRazorPages();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GenerateRandomString(int length)
|
||||||
|
{
|
||||||
|
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
|
var result = new StringBuilder(length);
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++)
|
||||||
|
{
|
||||||
|
var index = RandomNumberGenerator.GetInt32(chars.Length);
|
||||||
|
result.Append(chars[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5129",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace SpMega.Backend.Services;
|
||||||
|
|
||||||
|
public class TokenService(IConfiguration conf)
|
||||||
|
{
|
||||||
|
private const int AccessTokenExpirationMinutes = 24;
|
||||||
|
private readonly string _issuer = conf["JWT__Issuer"] ?? "spmega.il.yawaflua.tech";
|
||||||
|
private readonly string _secret = conf["JWT__Secret"] ?? throw new InvalidOperationException("JWT__Secret is not configured.");
|
||||||
|
|
||||||
|
public string GenerateAccessToken(string userName, Guid uuid)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new(JwtRegisteredClaimNames.Name, userName),
|
||||||
|
new (JwtRegisteredClaimNames.UniqueName, uuid.ToString()),
|
||||||
|
new(JwtRegisteredClaimNames.AuthTime, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()),
|
||||||
|
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||||
|
};
|
||||||
|
|
||||||
|
var tokenHandler = new JwtSecurityTokenHandler();
|
||||||
|
var key = Encoding.ASCII.GetBytes(_secret);
|
||||||
|
|
||||||
|
var tokenDescriptor = new SecurityTokenDescriptor
|
||||||
|
{
|
||||||
|
Subject = new ClaimsIdentity(claims),
|
||||||
|
Expires = DateTime.UtcNow.AddHours(AccessTokenExpirationMinutes),
|
||||||
|
Issuer = _issuer,
|
||||||
|
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature),
|
||||||
|
IssuedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
var token = tokenHandler.CreateToken(tokenDescriptor);
|
||||||
|
|
||||||
|
return tokenHandler.WriteToken(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="..\.dockerignore">
|
||||||
|
<Link>.dockerignore</Link>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Controllers\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Abstractions" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.9" />
|
||||||
|
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="8.19.1" />
|
||||||
|
<PackageReference Include="MongoDB.EntityFrameworkCore" Version="10.0.2" />
|
||||||
|
<PackageReference Include="spworlds-api" Version="1.0.3" />
|
||||||
|
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.19.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpMega.Backend", "SpMega.Backend\SpMega.Backend.csproj", "{FF126D84-9381-4F0C-9925-FE08AC1FD07E}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{64C061F2-C216-4C79-9AC2-CC43F1C0A34D}"
|
||||||
|
ProjectSection(SolutionItems) = preProject
|
||||||
|
compose.yaml = compose.yaml
|
||||||
|
EndProjectSection
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{FF126D84-9381-4F0C-9925-FE08AC1FD07E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
spmega.backend:
|
||||||
|
image: spmega.backend
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: SpMega.Backend/Dockerfile
|
||||||
|
|
||||||
+3
-7
@@ -28,17 +28,11 @@ fabricApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
// Add repositories to retrieve artifacts from in here.
|
|
||||||
// You should only use this when depending on other mods because
|
|
||||||
// Loom adds the essential maven repositories to download Minecraft and libraries from automatically.
|
|
||||||
// See https://docs.gradle.org/current/userguide/declaring_repositories.html
|
|
||||||
// for more information about repositories.
|
|
||||||
|
|
||||||
maven { url "https://maven.shedaniel.me/" }
|
maven { url "https://maven.shedaniel.me/" }
|
||||||
|
maven { url "https://maven.terraformersmc.com/" }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// To change the versions see the gradle.properties file
|
|
||||||
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
minecraft "com.mojang:minecraft:${project.minecraft_version}"
|
||||||
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2"
|
||||||
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
modImplementation "net.fabricmc:fabric-loader:${project.loader_version}"
|
||||||
@@ -49,6 +43,8 @@ dependencies {
|
|||||||
exclude(group: "net.fabricmc.fabric-api")
|
exclude(group: "net.fabricmc.fabric-api")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modCompileOnly "com.terraformersmc:modmenu:11.0.3"
|
||||||
|
|
||||||
include(implementation("org.xerial:sqlite-jdbc:3.46.1.3"))
|
include(implementation("org.xerial:sqlite-jdbc:3.46.1.3"))
|
||||||
include(clientImplementation("com.google.zxing:core:${project.zxing_version}"))
|
include(clientImplementation("com.google.zxing:core:${project.zxing_version}"))
|
||||||
include(clientImplementation("com.google.zxing:javase:${project.zxing_version}"))
|
include(clientImplementation("com.google.zxing:javase:${project.zxing_version}"))
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ minecraft_version=1.21.11
|
|||||||
yarn_mappings=1.21.11+build.4
|
yarn_mappings=1.21.11+build.4
|
||||||
loader_version=0.18.1
|
loader_version=0.18.1
|
||||||
# Mod Properties
|
# Mod Properties
|
||||||
mod_version=0.2-pre-alpha
|
mod_version=0.4-pre-alpha
|
||||||
maven_group=git.yawaflua.tech
|
maven_group=git.yawaflua.tech
|
||||||
archives_base_name=SPMega
|
archives_base_name=SPMega
|
||||||
# Dependencies
|
# Dependencies
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package git.yawaflua.tech.spmega.client;
|
||||||
|
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||||
|
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.ClickEvent;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class ChatListener {
|
||||||
|
private final UiNotifications notifications = UiNotifications.instance();
|
||||||
|
private final Pattern TRANSACTION_PATTERN = Pattern.compile(
|
||||||
|
".*(Управление картой).*",
|
||||||
|
Pattern.CASE_INSENSITIVE
|
||||||
|
);
|
||||||
|
|
||||||
|
public void register() {
|
||||||
|
ClientReceiveMessageEvents.CHAT.register((message, signedMessage, sender, params, timestamp) -> {
|
||||||
|
try {
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.err.println("[SPMega] Ошибка обработки сообщения в CHAT листенере:");
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ClientReceiveMessageEvents.GAME.register((message, overlay) -> {
|
||||||
|
try {
|
||||||
|
handleMessage(message);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.err.println("[SPMega] Ошибка обработки сообщения в GAME листенере:");
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMessage(Text message) {
|
||||||
|
if (message == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = message.getString();
|
||||||
|
if (TRANSACTION_PATTERN.matcher(text).matches()) {
|
||||||
|
List<String> clickValues = new ArrayList<>();
|
||||||
|
collectClickEventValues(message, clickValues);
|
||||||
|
|
||||||
|
if (clickValues.size() >= 2) {
|
||||||
|
String tokenId = clickValues.get(0);
|
||||||
|
String cardId = clickValues.get(1);
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.player != null) {
|
||||||
|
String playerUuid = client.player.getUuidAsString();
|
||||||
|
|
||||||
|
CompletableFuture
|
||||||
|
.supplyAsync(() -> BankUiService.instance().addCard(cardId, tokenId, playerUuid))
|
||||||
|
.thenAccept(msg -> {
|
||||||
|
if (client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
client.execute(() -> {
|
||||||
|
notifications.showMessage(msg);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.exceptionally(exception -> {
|
||||||
|
if (client != null) {
|
||||||
|
client.execute(() -> {
|
||||||
|
notifications.show(Text.literal("Ошибка добавления карты: " + exception.getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.out.println("[SPMega] Недостаточно кликабельных данных в сообщении (найдено: " + clickValues.size() + ")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collectClickEventValues(Text text, List<String> values) {
|
||||||
|
if (text == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickEvent clickEvent = text.getStyle().getClickEvent();
|
||||||
|
if (clickEvent != null) {
|
||||||
|
try {
|
||||||
|
String value = getEventValue(clickEvent);
|
||||||
|
if (value != null && !value.isEmpty()) {
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
System.err.println("[SPMega] Ошибка извлечения значения клик-события:");
|
||||||
|
t.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Text sibling : text.getSiblings()) {
|
||||||
|
collectClickEventValues(sibling, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getEventValue(ClickEvent clickEvent) {
|
||||||
|
if (clickEvent instanceof ClickEvent.CopyToClipboard(String value)) {
|
||||||
|
return value;
|
||||||
|
} else if (clickEvent instanceof ClickEvent.RunCommand(String command1)) {
|
||||||
|
return command1;
|
||||||
|
} else if (clickEvent instanceof ClickEvent.SuggestCommand(String command)) {
|
||||||
|
return command;
|
||||||
|
} else if (clickEvent instanceof ClickEvent.OpenUrl(java.net.URI uri)) {
|
||||||
|
return uri.toString();
|
||||||
|
} else if (clickEvent instanceof ClickEvent.OpenFile(String path)) {
|
||||||
|
return path;
|
||||||
|
} else if (clickEvent instanceof ClickEvent.ChangePage(int page)) {
|
||||||
|
return String.valueOf(page);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package git.yawaflua.tech.spmega.client;
|
||||||
|
|
||||||
|
import com.terraformersmc.modmenu.api.ConfigScreenFactory;
|
||||||
|
import com.terraformersmc.modmenu.api.ModMenuApi;
|
||||||
|
import git.yawaflua.tech.spmega.GpsHudPosition;
|
||||||
|
import git.yawaflua.tech.spmega.ModConfig;
|
||||||
|
import git.yawaflua.tech.spmega.SPMega;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
|
||||||
|
import me.shedaniel.clothconfig2.api.ConfigBuilder;
|
||||||
|
import me.shedaniel.clothconfig2.api.ConfigCategory;
|
||||||
|
import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.util.Formatting;
|
||||||
|
|
||||||
|
public class ModMenuIntegration implements ModMenuApi {
|
||||||
|
@Override
|
||||||
|
public ConfigScreenFactory<?> getModConfigScreenFactory() {
|
||||||
|
return parent -> {
|
||||||
|
ConfigBuilder builder = ConfigBuilder.create()
|
||||||
|
.setParentScreen(parent)
|
||||||
|
.setTitle(Text.translatable("title.spmega.config"));
|
||||||
|
|
||||||
|
ConfigEntryBuilder entryBuilder = builder.entryBuilder();
|
||||||
|
ConfigCategory general = builder.getOrCreateCategory(Text.translatable("category.spmega.general"));
|
||||||
|
|
||||||
|
ModConfig current = SPMega.getConfig();
|
||||||
|
if (current == null) {
|
||||||
|
current = ModConfig.createDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
final String[] apiDomain = {current.apiDomain()};
|
||||||
|
final String[] apiToken = {current.apiToken()};
|
||||||
|
final boolean[] allowBackend = {current.allowBackend()};
|
||||||
|
final boolean[] signQuickPayEnabled = {current.signQuickPayEnabled()};
|
||||||
|
final GpsHudPosition[] gpsPosition = {current.gpsPosition()};
|
||||||
|
|
||||||
|
general.addEntry(entryBuilder.startStrField(Text.translatable("option.spmega.api_domain"), current.apiDomain())
|
||||||
|
.setDefaultValue(ModConfig.DEFAULT_API_DOMAIN)
|
||||||
|
.setSaveConsumer(newValue -> apiDomain[0] = newValue)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
general.addEntry(entryBuilder.startStrField(Text.translatable("option.spmega.api_token"), current.apiToken())
|
||||||
|
.setDefaultValue(ModConfig.DEFAULT_API_TOKEN)
|
||||||
|
.setSaveConsumer(newValue -> apiToken[0] = newValue)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
general.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.allow_backend"), current.allowBackend())
|
||||||
|
.setDefaultValue(ModConfig.ALLOW_BACKEND)
|
||||||
|
.setSaveConsumer(newValue -> allowBackend[0] = newValue)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
boolean isLoggedIn = !current.apiToken().isEmpty() && !current.apiToken().equals(ModConfig.DEFAULT_API_TOKEN);
|
||||||
|
general.addEntry(entryBuilder.startBooleanToggle(Text.literal("Авторизоваться").formatted(Formatting.RED), isLoggedIn)
|
||||||
|
.setTooltip(Text.literal("Запустить процесс авторизации через Mojang и ваш бэкенд"))
|
||||||
|
.setSaveConsumer(newValue -> {
|
||||||
|
if (newValue) {
|
||||||
|
BackendAuthenticator.authenticate(MinecraftClient.getInstance());
|
||||||
|
if (SPMega.getConfig() != null) {
|
||||||
|
apiToken[0] = SPMega.getConfig().apiToken();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
apiToken[0] = ModConfig.DEFAULT_API_TOKEN;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build());
|
||||||
|
|
||||||
|
general.addEntry(entryBuilder.startBooleanToggle(Text.translatable("option.spmega.sign_quick_pay"), current.signQuickPayEnabled())
|
||||||
|
.setDefaultValue(ModConfig.DEFAULT_SIGN_QUICK_PAY_ENABLED)
|
||||||
|
.setSaveConsumer(newValue -> signQuickPayEnabled[0] = newValue)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
general.addEntry(entryBuilder.startEnumSelector(
|
||||||
|
Text.translatable("option.spmega.gps_position"),
|
||||||
|
GpsHudPosition.class,
|
||||||
|
current.gpsPosition()
|
||||||
|
)
|
||||||
|
.setDefaultValue(ModConfig.DEFAULT_GPS_POSITION)
|
||||||
|
.setEnumNameProvider(position -> Text.translatable("option.spmega.gps_position." + position.name().toLowerCase()))
|
||||||
|
.setSaveConsumer(newValue -> gpsPosition[0] = newValue)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
builder.setSavingRunnable(() -> {
|
||||||
|
boolean gpsEnabledVal = true;
|
||||||
|
if (SPMega.getConfig() != null) {
|
||||||
|
gpsEnabledVal = SPMega.getConfig().gpsEnabled();
|
||||||
|
}
|
||||||
|
ModConfig updated = new ModConfig(
|
||||||
|
apiDomain[0],
|
||||||
|
apiToken[0],
|
||||||
|
allowBackend[0],
|
||||||
|
signQuickPayEnabled[0],
|
||||||
|
gpsEnabledVal,
|
||||||
|
gpsPosition[0]
|
||||||
|
);
|
||||||
|
SPMega.setConfig(updated);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package git.yawaflua.tech.spmega.client;
|
package git.yawaflua.tech.spmega.client;
|
||||||
|
|
||||||
|
import git.yawaflua.tech.spmega.ModConfig;
|
||||||
import git.yawaflua.tech.spmega.SPMega;
|
import git.yawaflua.tech.spmega.SPMega;
|
||||||
import git.yawaflua.tech.spmega.client.qr.QRCodeScanner;
|
import git.yawaflua.tech.spmega.client.qr.QRCodeScanner;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.GpsHudRenderer;
|
||||||
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
import git.yawaflua.tech.spmega.client.ui.UiNotifications;
|
||||||
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
import git.yawaflua.tech.spmega.client.ui.UiOpeners;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
|
||||||
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||||
import net.fabricmc.api.ClientModInitializer;
|
import net.fabricmc.api.ClientModInitializer;
|
||||||
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
|
||||||
@@ -18,6 +21,7 @@ import net.minecraft.client.MinecraftClient;
|
|||||||
import net.minecraft.client.option.KeyBinding;
|
import net.minecraft.client.option.KeyBinding;
|
||||||
import net.minecraft.client.util.InputUtil;
|
import net.minecraft.client.util.InputUtil;
|
||||||
import net.minecraft.entity.decoration.ItemFrameEntity;
|
import net.minecraft.entity.decoration.ItemFrameEntity;
|
||||||
|
import net.minecraft.entity.player.PlayerEntity;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
import net.minecraft.util.ActionResult;
|
import net.minecraft.util.ActionResult;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
@@ -29,6 +33,9 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(?<!\\d)(\\d{5})(?!\\d)");
|
private static final Pattern ISOLATED_FIVE_DIGITS = Pattern.compile("(?<!\\d)(\\d{5})(?!\\d)");
|
||||||
private static KeyBinding openBankMenuKeyBinding;
|
private static KeyBinding openBankMenuKeyBinding;
|
||||||
private static KeyBinding scanQrKeyBinding;
|
private static KeyBinding scanQrKeyBinding;
|
||||||
|
private static KeyBinding toggleGpsKeyBinding;
|
||||||
|
private static boolean wasPaymentShortcutPressed = false;
|
||||||
|
|
||||||
|
|
||||||
private static String extractCardNumber(SignBlockEntity signBlockEntity) {
|
private static String extractCardNumber(SignBlockEntity signBlockEntity) {
|
||||||
String candidate = findFiveDigits(signBlockEntity.getFrontText().getMessages(false));
|
String candidate = findFiveDigits(signBlockEntity.getFrontText().getMessages(false));
|
||||||
@@ -65,6 +72,7 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitializeClient() {
|
public void onInitializeClient() {
|
||||||
|
GpsHudRenderer.instance();
|
||||||
openBankMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
openBankMenuKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
||||||
"key.spmega.open_menu",
|
"key.spmega.open_menu",
|
||||||
InputUtil.Type.KEYSYM,
|
InputUtil.Type.KEYSYM,
|
||||||
@@ -79,6 +87,17 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
KeyBinding.Category.GAMEPLAY
|
KeyBinding.Category.GAMEPLAY
|
||||||
));
|
));
|
||||||
|
|
||||||
|
toggleGpsKeyBinding = KeyBindingHelper.registerKeyBinding(new KeyBinding(
|
||||||
|
"key.spmega.toggle_gps",
|
||||||
|
InputUtil.Type.KEYSYM,
|
||||||
|
GLFW.GLFW_KEY_J,
|
||||||
|
KeyBinding.Category.GAMEPLAY
|
||||||
|
));
|
||||||
|
|
||||||
|
if (SPMega.getConfig() != null) {
|
||||||
|
GpsHudRenderer.instance().setEnabled(SPMega.getConfig().gpsEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
ClientTickEvents.END_CLIENT_TICK.register(client -> {
|
||||||
UiNotifications.instance().tick();
|
UiNotifications.instance().tick();
|
||||||
while (openBankMenuKeyBinding.wasPressed()) {
|
while (openBankMenuKeyBinding.wasPressed()) {
|
||||||
@@ -87,9 +106,40 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
while (scanQrKeyBinding.wasPressed()) {
|
while (scanQrKeyBinding.wasPressed()) {
|
||||||
QRCodeScanner.ScanQrCode(client);
|
QRCodeScanner.ScanQrCode(client);
|
||||||
}
|
}
|
||||||
|
while (toggleGpsKeyBinding.wasPressed()) {
|
||||||
|
GpsHudRenderer.instance().toggle();
|
||||||
|
if (SPMega.getConfig() != null) {
|
||||||
|
ModConfig current = SPMega.getConfig();
|
||||||
|
ModConfig updated = new ModConfig(
|
||||||
|
current.apiDomain(),
|
||||||
|
current.apiToken(),
|
||||||
|
current.allowBackend(),
|
||||||
|
current.signQuickPayEnabled(),
|
||||||
|
GpsHudRenderer.instance().isEnabled(),
|
||||||
|
current.gpsPosition()
|
||||||
|
);
|
||||||
|
SPMega.setConfig(updated);
|
||||||
|
}
|
||||||
|
String status = GpsHudRenderer.instance().isEnabled() ? "включен" : "выключен";
|
||||||
|
UiNotifications.instance().show(Text.literal("GPS Ада " + status));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.player != null && client.options != null) {
|
||||||
|
boolean isCombinationPressed = client.options.sprintKey.isPressed() && client.options.sneakKey.isPressed() && client.options.leftKey.isPressed();
|
||||||
|
if (isCombinationPressed) {
|
||||||
|
if (!wasPaymentShortcutPressed && client.currentScreen == null) {
|
||||||
|
if (client.targetedEntity instanceof PlayerEntity targetedPlayer && targetedPlayer != client.player) {
|
||||||
|
String recipient = targetedPlayer.getStringifiedName();
|
||||||
|
UiOpeners.openPaymentMenu(client, recipient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wasPaymentShortcutPressed = true;
|
||||||
|
} else {
|
||||||
|
wasPaymentShortcutPressed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// World HUD path (when no screen is open).
|
|
||||||
HudRenderCallback.EVENT.register((drawContext, tickDeltaManager) -> {
|
HudRenderCallback.EVENT.register((drawContext, tickDeltaManager) -> {
|
||||||
MinecraftClient client = MinecraftClient.getInstance();
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
if (client.currentScreen != null || client.textRenderer == null) {
|
if (client.currentScreen != null || client.textRenderer == null) {
|
||||||
@@ -101,16 +151,35 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
client.getWindow().getScaledWidth(),
|
client.getWindow().getScaledWidth(),
|
||||||
client.getWindow().getScaledHeight()
|
client.getWindow().getScaledHeight()
|
||||||
);
|
);
|
||||||
|
GpsHudRenderer.instance().render(
|
||||||
|
drawContext,
|
||||||
|
client.textRenderer,
|
||||||
|
client.getWindow().getScaledWidth(),
|
||||||
|
client.getWindow().getScaledHeight()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Screen path (for all GUI screens).
|
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
|
||||||
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) ->
|
|
||||||
ScreenEvents.afterRender(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> {
|
ScreenEvents.afterRender(screen).register((screenInstance, drawContext, mouseX, mouseY, tickDelta) -> {
|
||||||
if (client.textRenderer == null) {
|
if (client.textRenderer == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
UiNotifications.instance().render(drawContext, client.textRenderer, scaledWidth, scaledHeight);
|
UiNotifications.instance().render(drawContext, client.textRenderer, scaledWidth, scaledHeight);
|
||||||
})
|
});
|
||||||
|
// CompletableFuture.supplyAsync(() -> BackendAuthenticator.authenticate(client))
|
||||||
|
// .thenAccept(authResult -> {
|
||||||
|
// if (authResult) {
|
||||||
|
// System.out.println("SPMega: Authenticated on backend successfully.");
|
||||||
|
// } else {
|
||||||
|
// System.err.println("SPMega: Authentication on backend failed");
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// .exceptionally(ex -> {
|
||||||
|
// System.err.println("SPMega: Authentication error: " + ex.getMessage());
|
||||||
|
// return null;
|
||||||
|
// });
|
||||||
|
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> {
|
||||||
@@ -167,5 +236,10 @@ public class SPMegaClient implements ClientModInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new ChatListener().register();
|
||||||
|
BackendAuthenticator.authenticate(MinecraftClient.getInstance());
|
||||||
|
System.out.println("Author of SPMega make it with 4 cans of monster");
|
||||||
|
System.out.println("Initialized beshalom! Tieie tovim!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import net.minecraft.text.Text;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class CardScreen extends Screen {
|
public class CardScreen extends Screen {
|
||||||
private final Screen parent;
|
private final Screen parent;
|
||||||
private final BankUiService bankUiService = BankUiService.instance();
|
private final BankUiService bankUiService = BankUiService.instance();
|
||||||
private final UiNotifications notifications = UiNotifications.instance();
|
private final UiNotifications notifications = UiNotifications.instance();
|
||||||
private final List<ButtonWidget> cardButtons = new ArrayList<>();
|
private final List<ButtonWidget> cardButtons = new ArrayList<>();
|
||||||
|
private ButtonWidget historyButton;
|
||||||
|
|
||||||
public CardScreen(Screen parent) {
|
public CardScreen(Screen parent) {
|
||||||
super(Text.literal("Управление картами"));
|
super(Text.literal("Управление картами"));
|
||||||
@@ -44,16 +46,22 @@ public class CardScreen extends Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Удалить"), button -> {
|
this.addDrawableChild(ButtonWidget.builder(Text.literal("Удалить"), button -> {
|
||||||
|
CompletableFuture.supplyAsync(() -> {
|
||||||
bankUiService.removeSelectedCard();
|
bankUiService.removeSelectedCard();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
notifications.showMessage(bankUiService.getLastMessage());
|
notifications.showMessage(bankUiService.getLastMessage());
|
||||||
this.clearAndInit();
|
this.clearAndInit();
|
||||||
}).dimensions(rightX, startY, columnWidth, 20).build());
|
}).dimensions(rightX, startY, columnWidth, 20).build());
|
||||||
|
|
||||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Изменить"), button -> {
|
this.addDrawableChild(ButtonWidget.builder(Text.literal("Обновить"), button -> {
|
||||||
String playerUuid = this.client != null && this.client.player != null
|
String playerUuid = this.client != null && this.client.player != null
|
||||||
? this.client.player.getUuidAsString()
|
? this.client.player.getUuidAsString()
|
||||||
: "";
|
: "";
|
||||||
|
CompletableFuture.supplyAsync(() -> {
|
||||||
bankUiService.refreshSelectedCard(playerUuid);
|
bankUiService.refreshSelectedCard(playerUuid);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
String message = bankUiService.getLastMessage().isBlank() ? "Карта обновлена" : bankUiService.getLastMessage();
|
String message = bankUiService.getLastMessage().isBlank() ? "Карта обновлена" : bankUiService.getLastMessage();
|
||||||
notifications.showMessage(message);
|
notifications.showMessage(message);
|
||||||
this.clearAndInit();
|
this.clearAndInit();
|
||||||
@@ -69,6 +77,18 @@ public class CardScreen extends Screen {
|
|||||||
}));
|
}));
|
||||||
}).dimensions(rightX, startY + 48, columnWidth, 20).build());
|
}).dimensions(rightX, startY + 48, columnWidth, 20).build());
|
||||||
|
|
||||||
|
historyButton = this.addDrawableChild(ButtonWidget.builder(Text.literal("История"), button -> {
|
||||||
|
if (this.client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CardViewModel selected = bankUiService.getSelectedCard();
|
||||||
|
if (selected == null) {
|
||||||
|
notifications.show(Text.literal("Сначала выбери или добавь карту"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.setScreen(new TransactionHistoryScreen(this, selected.id(), selected.title()));
|
||||||
|
}).dimensions(rightX, startY + 72, columnWidth, 20).build());
|
||||||
|
|
||||||
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||||
.dimensions(rightX, startY + 120, columnWidth, 20)
|
.dimensions(rightX, startY + 120, columnWidth, 20)
|
||||||
.build());
|
.build());
|
||||||
@@ -119,5 +139,9 @@ public class CardScreen extends Screen {
|
|||||||
button.setMessage(Text.literal(prefix + cards.get(i).title() + suffix));
|
button.setMessage(Text.literal(prefix + cards.get(i).title() + suffix));
|
||||||
button.active = !selected;
|
button.active = !selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (historyButton != null) {
|
||||||
|
historyButton.active = !cards.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,238 @@
|
|||||||
|
package git.yawaflua.tech.spmega.client.ui;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import git.yawaflua.tech.spmega.GpsHudPosition;
|
||||||
|
import git.yawaflua.tech.spmega.SPMega;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.font.TextRenderer;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
import net.minecraft.world.World;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public final class GpsHudRenderer {
|
||||||
|
private static final GpsHudRenderer INSTANCE = new GpsHudRenderer();
|
||||||
|
private final Object citiesLock = new Object();
|
||||||
|
private boolean enabled = true;
|
||||||
|
private List<City> cities = new ArrayList<>();
|
||||||
|
|
||||||
|
private GpsHudRenderer() {
|
||||||
|
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||||
|
Thread thread = new Thread(r, "SPMega-City-Poller");
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
});
|
||||||
|
scheduler.scheduleAtFixedRate(this::fetchCities, 0, 5, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GpsHudRenderer instance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toggle() {
|
||||||
|
this.enabled = !this.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchCities() {
|
||||||
|
try {
|
||||||
|
HttpClient client = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create("https://spm-map.tonyaleksandr.ru/api/map/territories"))
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() == 200) {
|
||||||
|
TerritoryWrapper[] parsed = new Gson().fromJson(response.body(), TerritoryWrapper[].class);
|
||||||
|
if (parsed != null) {
|
||||||
|
List<City> list = new ArrayList<>();
|
||||||
|
for (TerritoryWrapper w : parsed) {
|
||||||
|
if (w != null && w.territory != null && w.territory.nether_portal != null && w.territory.nether_portal.length >= 2) {
|
||||||
|
City c = new City();
|
||||||
|
c.name = w.territory.name;
|
||||||
|
c.netherX = w.territory.nether_portal[0];
|
||||||
|
c.netherZ = w.territory.nether_portal[1];
|
||||||
|
list.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
synchronized (citiesLock) {
|
||||||
|
cities = list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void render(DrawContext context, TextRenderer textRenderer, int width, int height) {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftClient client = MinecraftClient.getInstance();
|
||||||
|
if (client.world == null || client.player == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.world.getRegistryKey() != World.NETHER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double playerX = client.player.getX();
|
||||||
|
double playerZ = client.player.getZ();
|
||||||
|
|
||||||
|
LaneInfo playerLane = new LaneInfo(Math.round(playerX), Math.round(playerZ));
|
||||||
|
|
||||||
|
String titleText = "§7Ветка: " + playerLane.name + " §7(§f" + playerLane.coord + "§7)";
|
||||||
|
String offsetText = "§7Смещение: §f" + (playerLane.offset == 0 ? "0" : (playerLane.offset > 0 ? "+" + playerLane.offset : playerLane.offset));
|
||||||
|
|
||||||
|
City nearestCity = null;
|
||||||
|
double minDistanceSq = Double.MAX_VALUE;
|
||||||
|
for (City city : cities) {
|
||||||
|
double dx = playerX - (city.netherX / 8);
|
||||||
|
double dz = playerZ - (city.netherZ / 8);
|
||||||
|
double distSq = Math.sqrt(dx * dx + dz * dz);
|
||||||
|
if (distSq < minDistanceSq) {
|
||||||
|
minDistanceSq = distSq;
|
||||||
|
nearestCity = city;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String cityText = null;
|
||||||
|
if (nearestCity != null) {
|
||||||
|
long cx = Math.round(nearestCity.netherX / 8.0);
|
||||||
|
long cz = Math.round(nearestCity.netherZ / 8.0);
|
||||||
|
LaneInfo cityLane = new LaneInfo(cx, cz);
|
||||||
|
cityText = "§7Ближайший: §f" + nearestCity.name + " §7~(§f" + cityLane.name + " §f" + cityLane.coord + "§7, §7см: §f" + (cityLane.offset == 0 ? "0" : (cityLane.offset > 0 ? "+" + cityLane.offset : cityLane.offset)) + "§7)";
|
||||||
|
}
|
||||||
|
|
||||||
|
int padding = 6;
|
||||||
|
int lineSpacing = 2;
|
||||||
|
int textWidth = Math.max(textRenderer.getWidth(Text.literal(titleText)), textRenderer.getWidth(Text.literal(offsetText)));
|
||||||
|
if (cityText != null) {
|
||||||
|
textWidth = Math.max(textWidth, textRenderer.getWidth(Text.literal(cityText)));
|
||||||
|
}
|
||||||
|
|
||||||
|
int boxWidth = textWidth + padding * 2 + 6;
|
||||||
|
int linesCount = cityText != null ? 3 : 2;
|
||||||
|
int boxHeight = textRenderer.fontHeight * linesCount + lineSpacing * (linesCount - 1) + padding * 2;
|
||||||
|
|
||||||
|
GpsHudPosition position = GpsHudPosition.TOP_CENTER;
|
||||||
|
if (SPMega.getConfig() != null) {
|
||||||
|
position = SPMega.getConfig().gpsPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
int boxX;
|
||||||
|
int boxY;
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case TOP_RIGHT:
|
||||||
|
boxX = width - boxWidth - 10;
|
||||||
|
boxY = 10;
|
||||||
|
break;
|
||||||
|
case BOTTOM_LEFT:
|
||||||
|
boxX = 10;
|
||||||
|
boxY = height - boxHeight - 10;
|
||||||
|
break;
|
||||||
|
case BOTTOM_RIGHT:
|
||||||
|
boxX = width - boxWidth - 10;
|
||||||
|
boxY = height - boxHeight - 10;
|
||||||
|
break;
|
||||||
|
case TOP_CENTER:
|
||||||
|
boxX = (width - boxWidth) / 2;
|
||||||
|
boxY = 10;
|
||||||
|
break;
|
||||||
|
case BOTTOM_CENTER:
|
||||||
|
boxX = (width - boxWidth) / 2;
|
||||||
|
boxY = height - boxHeight - 10;
|
||||||
|
break;
|
||||||
|
case TOP_LEFT:
|
||||||
|
default:
|
||||||
|
boxX = 10;
|
||||||
|
boxY = 10;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bgCol = 0x90101010;
|
||||||
|
context.fill(boxX, boxY, boxX + boxWidth, boxY + boxHeight, bgCol);
|
||||||
|
|
||||||
|
int barWidth = 3;
|
||||||
|
context.fill(boxX + padding, boxY + padding, boxX + padding + barWidth, boxY + boxHeight - padding, playerLane.color);
|
||||||
|
|
||||||
|
int textX = boxX + padding + barWidth + 4;
|
||||||
|
int textY = boxY + padding;
|
||||||
|
context.drawTextWithShadow(textRenderer, Text.literal(titleText), textX, textY, 0xFFFFFFFF);
|
||||||
|
context.drawTextWithShadow(textRenderer, Text.literal(offsetText), textX, textY + textRenderer.fontHeight + lineSpacing, 0xFFFFFFFF);
|
||||||
|
if (cityText != null) {
|
||||||
|
context.drawTextWithShadow(textRenderer, Text.literal(cityText), textX, textY + (textRenderer.fontHeight + lineSpacing) * 2, 0xFFFFFFFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class City {
|
||||||
|
String name;
|
||||||
|
int netherX;
|
||||||
|
int netherZ;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TerritoryWrapper {
|
||||||
|
Territory territory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Territory {
|
||||||
|
String name;
|
||||||
|
int[] nether_portal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class LaneInfo {
|
||||||
|
String name;
|
||||||
|
int color;
|
||||||
|
long coord;
|
||||||
|
long offset;
|
||||||
|
|
||||||
|
LaneInfo(long x, long z) {
|
||||||
|
long absX = Math.abs(x);
|
||||||
|
long absZ = Math.abs(z);
|
||||||
|
if (absX > absZ) {
|
||||||
|
if (x > 0) {
|
||||||
|
name = "§aЗеленая";
|
||||||
|
color = 0xFF55FF55;
|
||||||
|
coord = x;
|
||||||
|
offset = z;
|
||||||
|
} else {
|
||||||
|
name = "§9Синяя";
|
||||||
|
color = 0xFF5555FF;
|
||||||
|
coord = absX;
|
||||||
|
offset = z;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (z < 0) {
|
||||||
|
name = "§cКрасная";
|
||||||
|
color = 0xFFFF5555;
|
||||||
|
coord = absZ;
|
||||||
|
offset = x;
|
||||||
|
} else {
|
||||||
|
name = "§eЖелтая";
|
||||||
|
color = 0xFFFFFF55;
|
||||||
|
coord = absZ;
|
||||||
|
offset = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ public class PaymentScreen extends Screen {
|
|||||||
private Text senderCardText = Text.literal("Карта отправителя: не выбрана");
|
private Text senderCardText = Text.literal("Карта отправителя: не выбрана");
|
||||||
private ButtonWidget transferButton;
|
private ButtonWidget transferButton;
|
||||||
private ButtonWidget backButton;
|
private ButtonWidget backButton;
|
||||||
|
private boolean transferInProgress;
|
||||||
|
|
||||||
public PaymentScreen(Screen parent) {
|
public PaymentScreen(Screen parent) {
|
||||||
this(parent, "");
|
this(parent, "");
|
||||||
@@ -192,6 +193,11 @@ public class PaymentScreen extends Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void submit() {
|
private void submit() {
|
||||||
|
if (transferInProgress) {
|
||||||
|
notifications.show(Text.literal("Перевод уже выполняется"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||||
if (selectedCard == null) {
|
if (selectedCard == null) {
|
||||||
notifications.show(Text.literal("Нет выбранной карты отправителя"));
|
notifications.show(Text.literal("Нет выбранной карты отправителя"));
|
||||||
@@ -205,7 +211,8 @@ public class PaymentScreen extends Screen {
|
|||||||
notifications.show(Text.literal("Сумма должна быть больше 0"));
|
notifications.show(Text.literal("Сумма должна быть больше 0"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException exception) {
|
} catch (Exception exception) {
|
||||||
|
System.out.println(exception.getMessage());
|
||||||
notifications.show(Text.literal("Некорректная сумма"));
|
notifications.show(Text.literal("Некорректная сумма"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -232,18 +239,46 @@ public class PaymentScreen extends Screen {
|
|||||||
commentField.getText().trim()
|
commentField.getText().trim()
|
||||||
);
|
);
|
||||||
|
|
||||||
boolean accepted = bankUiService.submitPayment(draft);
|
setTransferInProgress(true);
|
||||||
|
notifications.show(Text.literal("Отправка перевода..."));
|
||||||
|
|
||||||
|
bankUiService.submitPaymentAsync(draft)
|
||||||
|
.thenAccept(accepted -> {
|
||||||
|
if (this.client == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.client.execute(() -> {
|
||||||
|
setTransferInProgress(false);
|
||||||
|
|
||||||
String serviceMessage = bankUiService.getLastMessage();
|
String serviceMessage = bankUiService.getLastMessage();
|
||||||
if (!serviceMessage.isBlank()) {
|
if (!serviceMessage.isBlank()) {
|
||||||
|
|
||||||
notifications.showMessage(serviceMessage);
|
notifications.showMessage(serviceMessage);
|
||||||
} else {
|
} else {
|
||||||
notifications.show(accepted
|
notifications.show(accepted
|
||||||
? Text.literal("Перевод выполнен")
|
? Text.literal("Перевод выполнен")
|
||||||
: Text.literal("Перевод отклонен"));
|
: Text.literal("Перевод отклонен"));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSenderCardSelector();
|
updateSenderCardSelector();
|
||||||
updateSenderCardText();
|
updateSenderCardText();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.exceptionally(exception -> {
|
||||||
|
if (this.client != null) {
|
||||||
|
this.client.execute(() -> {
|
||||||
|
setTransferInProgress(false);
|
||||||
|
notifications.show(Text.literal("Ошибка перевода: " + exception.getMessage()));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTransferInProgress(boolean inProgress) {
|
||||||
|
transferInProgress = inProgress;
|
||||||
|
if (transferButton != null) {
|
||||||
|
transferButton.active = !inProgress;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isValidRecipient(String recipient) {
|
private boolean isValidRecipient(String recipient) {
|
||||||
@@ -277,7 +312,7 @@ public class PaymentScreen extends Screen {
|
|||||||
private void updateSenderCardSelector() {
|
private void updateSenderCardSelector() {
|
||||||
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
CardViewModel selectedCard = bankUiService.getSelectedCard();
|
||||||
if (selectedCard == null) {
|
if (selectedCard == null) {
|
||||||
senderCardLabelButton.setMessage(Text.literal("\"00000\": \"0\" АР"));
|
senderCardLabelButton.setMessage(Text.literal("TEST 00000: 0 АР"));
|
||||||
senderLeftButton.active = false;
|
senderLeftButton.active = false;
|
||||||
senderRightButton.active = false;
|
senderRightButton.active = false;
|
||||||
return;
|
return;
|
||||||
@@ -285,9 +320,8 @@ public class PaymentScreen extends Screen {
|
|||||||
|
|
||||||
senderLeftButton.active = true;
|
senderLeftButton.active = true;
|
||||||
senderRightButton.active = true;
|
senderRightButton.active = true;
|
||||||
String cardId = extractCardId(selectedCard.title());
|
|
||||||
String balance = Long.toString(selectedCard.balance());
|
String balance = Long.toString(selectedCard.balance());
|
||||||
senderCardLabelButton.setMessage(Text.literal("\"" + cardId + "\": \"" + balance + "\" АР"));
|
senderCardLabelButton.setMessage(Text.literal(selectedCard.title() + ": " + balance + " АР"));
|
||||||
senderCardLabelButton.active = false;
|
senderCardLabelButton.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import net.minecraft.client.gui.widget.ButtonWidget;
|
|||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
import net.minecraft.util.Util;
|
import net.minecraft.util.Util;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
public class QRcodeAcceptScreen extends Screen {
|
public class QRcodeAcceptScreen extends Screen {
|
||||||
@@ -20,6 +21,8 @@ public class QRcodeAcceptScreen extends Screen {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void init() {
|
protected void init() {
|
||||||
|
|
||||||
|
|
||||||
this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> {
|
this.addDrawableChild(ButtonWidget.builder(Text.translatable("button.spmega.qr.cancel"), button -> {
|
||||||
if (this.client != null) {
|
if (this.client != null) {
|
||||||
this.client.setScreen(parent);
|
this.client.setScreen(parent);
|
||||||
@@ -53,17 +56,10 @@ public class QRcodeAcceptScreen extends Screen {
|
|||||||
|
|
||||||
context.drawCenteredTextWithShadow(
|
context.drawCenteredTextWithShadow(
|
||||||
this.textRenderer,
|
this.textRenderer,
|
||||||
Text.translatable("screen.spmega.qr.accept_link"),
|
Text.literal("Найдена ссылка: " + url),
|
||||||
this.width / 2,
|
this.width / 2,
|
||||||
this.height / 2 - 30,
|
this.height / 2,
|
||||||
0xFFFFFF
|
Color.WHITE.getRGB()
|
||||||
);
|
|
||||||
context.drawCenteredTextWithShadow(
|
|
||||||
this.textRenderer,
|
|
||||||
Text.literal(url),
|
|
||||||
this.width / 2,
|
|
||||||
this.height / 2 - 10,
|
|
||||||
0xFFD27F
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package git.yawaflua.tech.spmega.client.ui;
|
||||||
|
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BackendAuthenticator;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BankDatabase.LocalTransaction;
|
||||||
|
import git.yawaflua.tech.spmega.client.ui.service.BankUiService;
|
||||||
|
import net.minecraft.client.gui.DrawContext;
|
||||||
|
import net.minecraft.client.gui.screen.Screen;
|
||||||
|
import net.minecraft.client.gui.widget.ButtonWidget;
|
||||||
|
import net.minecraft.text.Text;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public class TransactionHistoryScreen extends Screen {
|
||||||
|
private final Screen parent;
|
||||||
|
private final String cardId;
|
||||||
|
private final String cardTitle;
|
||||||
|
private final List<LocalTransaction> transactions = new ArrayList<>();
|
||||||
|
private boolean loading = true;
|
||||||
|
private String errorMessage = "";
|
||||||
|
|
||||||
|
public TransactionHistoryScreen(Screen parent, String cardId, String cardTitle) {
|
||||||
|
super(Text.literal("История транзакций"));
|
||||||
|
this.parent = parent;
|
||||||
|
this.cardId = cardId;
|
||||||
|
this.cardTitle = cardTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void init() {
|
||||||
|
int centerX = this.width / 2;
|
||||||
|
int startY = this.height / 2 + 50;
|
||||||
|
|
||||||
|
this.addDrawableChild(ButtonWidget.builder(Text.literal("Назад"), button -> this.close())
|
||||||
|
.dimensions(centerX - 60, startY + 10, 120, 20)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
loadTransactions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadTransactions() {
|
||||||
|
loading = true;
|
||||||
|
errorMessage = "";
|
||||||
|
transactions.clear();
|
||||||
|
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
List<LocalTransaction> list = null;
|
||||||
|
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
||||||
|
if (config != null && config.allowBackend()) {
|
||||||
|
try {
|
||||||
|
list = BackendAuthenticator.fetchTransactionsFromBackend(cardId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[SPMEGA] Failed to fetch transactions from server, falling back to DB: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list == null) {
|
||||||
|
list = BankUiService.instance().getDatabase().loadTransferHistory(cardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<LocalTransaction> finalList = list;
|
||||||
|
if (this.client != null) {
|
||||||
|
this.client.execute(() -> {
|
||||||
|
this.transactions.addAll(finalList);
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (this.client != null) {
|
||||||
|
this.client.execute(() -> {
|
||||||
|
this.errorMessage = e.getMessage();
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (this.client != null) {
|
||||||
|
this.client.setScreen(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
|
||||||
|
super.render(context, mouseX, mouseY, delta);
|
||||||
|
|
||||||
|
int centerX = this.width / 2;
|
||||||
|
int startY = 50;
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(this.textRenderer, this.title, centerX, 20, 0xFFFFFF);
|
||||||
|
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Карта: " + cardTitle), centerX, 35, 0xBFBFBF);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Загрузка транзакций..."), centerX, this.height / 2 - 10, 0xCCCCCC);
|
||||||
|
} else if (!errorMessage.isEmpty()) {
|
||||||
|
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Ошибка: " + errorMessage), centerX, this.height / 2 - 10, 0xFF5555);
|
||||||
|
} else if (transactions.isEmpty()) {
|
||||||
|
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal("Транзакций не найдено"), centerX, this.height / 2 - 10, 0xCCCCCC);
|
||||||
|
} else {
|
||||||
|
int y = startY + 15;
|
||||||
|
for (int i = 0; i < Math.min(6, transactions.size()); i++) {
|
||||||
|
LocalTransaction tx = transactions.get(i);
|
||||||
|
|
||||||
|
String amountText = tx.amount() + " АР";
|
||||||
|
String dateText = tx.createdAt();
|
||||||
|
if (dateText.length() > 19) {
|
||||||
|
dateText = dateText.substring(0, 19).replace("T", " ");
|
||||||
|
}
|
||||||
|
String commentText = tx.comment().isEmpty() ? "" : " (" + tx.comment() + ")";
|
||||||
|
|
||||||
|
String line = String.format("%s -> %s | %s%s", dateText, tx.receiver(), amountText, commentText);
|
||||||
|
int color = tx.status().equalsIgnoreCase("SUCCESS") ? 0x55FF55 : 0xFF5555;
|
||||||
|
|
||||||
|
context.drawCenteredTextWithShadow(this.textRenderer, Text.literal(line), centerX, y, color);
|
||||||
|
y += 18;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
package git.yawaflua.tech.spmega.client.ui.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import com.mojang.authlib.exceptions.AuthenticationException;
|
||||||
|
import com.mojang.authlib.minecraft.MinecraftSessionService;
|
||||||
|
import git.yawaflua.tech.spmega.ModConfig;
|
||||||
|
import git.yawaflua.tech.spmega.SPMega;
|
||||||
|
import net.minecraft.client.MinecraftClient;
|
||||||
|
import net.minecraft.client.session.Session;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class BackendAuthenticator {
|
||||||
|
|
||||||
|
public static boolean authenticate(MinecraftClient client) {
|
||||||
|
Session session = client.getSession();
|
||||||
|
if (session == null || session.getUuidOrNull() == null) {
|
||||||
|
System.err.println("Cannot authenticate: Client has no valid session.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MinecraftSessionService sessionService = client.getApiServices().sessionService();
|
||||||
|
|
||||||
|
try {
|
||||||
|
var serverId = sendStartSessionRequestToBackend(session.getUsername(), session.getUuidOrNull());
|
||||||
|
System.out.println("[SPMEGA] Trying to auth in mojang with serverId: " + serverId);
|
||||||
|
sessionService.joinServer(
|
||||||
|
session.getUuidOrNull(),
|
||||||
|
session.getAccessToken(),
|
||||||
|
serverId
|
||||||
|
);
|
||||||
|
System.out.println("[SPMEGA] Sending session submitter to backend");
|
||||||
|
|
||||||
|
return sendAuthRequestToBackend(session.getUuidOrNull(), serverId);
|
||||||
|
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
System.err.println("I cant auth by Mojang: " + e.getMessage());
|
||||||
|
System.err.println("Please check your credentials and try again.");
|
||||||
|
return false;
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Failed to authenticate with backend: " + e.getMessage());
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sendStartSessionRequestToBackend(String username, UUID uuid) throws IOException, InterruptedException {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject jsonPayload = new JsonObject();
|
||||||
|
jsonPayload.addProperty("userName", username);
|
||||||
|
jsonPayload.addProperty("userUUID", uuid.toString());
|
||||||
|
String requestBody = jsonPayload.toString();
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/auth/start";
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.method("POST", HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on start: " + response.body());
|
||||||
|
throw new IOException("Server returned status code " + response.statusCode() + " on start: " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||||
|
if (json.has("sessionId")) {
|
||||||
|
return json.get("sessionId").getAsString();
|
||||||
|
} else {
|
||||||
|
System.err.println("[SPMEGA] Invalid response from start endpoint: " + response.body());
|
||||||
|
throw new IOException("Invalid response from start endpoint: " + response.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sendAuthRequestToBackend(UUID uuid, String serverId) throws IOException, InterruptedException {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
String apiDomain = (config != null) ? config.apiDomain() : ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject jsonPayload = new JsonObject();
|
||||||
|
jsonPayload.addProperty("userUUID", uuid.toString());
|
||||||
|
jsonPayload.addProperty("sessionId", serverId);
|
||||||
|
String requestBody = jsonPayload.toString();
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/auth/validate";
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
System.err.println("[SPMEGA] Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
||||||
|
throw new IOException("Server returned status code " + response.statusCode() + " on validate: " + response.body());
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObject json = JsonParser.parseString(response.body()).getAsJsonObject();
|
||||||
|
if (json.has("token")) {
|
||||||
|
String token = json.get("token").getAsString();
|
||||||
|
if (config != null) {
|
||||||
|
ModConfig updated = new ModConfig(
|
||||||
|
config.apiDomain(),
|
||||||
|
token,
|
||||||
|
config.allowBackend(),
|
||||||
|
config.signQuickPayEnabled(),
|
||||||
|
config.gpsEnabled(),
|
||||||
|
config.gpsPosition()
|
||||||
|
);
|
||||||
|
SPMega.setConfig(updated);
|
||||||
|
System.out.println("[SPMEGA] Backend auth successful, saved token.");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
System.err.println("[SPMEGA] Config is null, cannot save token.");
|
||||||
|
throw new IOException("Config is null, cannot save token.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.err.println("[SPMEGA] Invalid response from validate endpoint: " + response.body());
|
||||||
|
throw new IOException("Invalid response from validate endpoint: " + response.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void sendCardToBackend(String cardId, String cardToken) {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null || !config.allowBackend()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiDomain = config.apiDomain();
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/auth/cards";
|
||||||
|
|
||||||
|
JsonObject jsonPayload = new JsonObject();
|
||||||
|
jsonPayload.addProperty("id", cardId);
|
||||||
|
jsonPayload.addProperty("token", cardToken);
|
||||||
|
String requestBody = jsonPayload.toString();
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
.thenAccept(response -> {
|
||||||
|
if (response.statusCode() != 200 && response.statusCode() != 204) {
|
||||||
|
System.err.println("[SPMEGA] Failed to send card to backend: status code "
|
||||||
|
+ response.statusCode() + ", body: " + response.body());
|
||||||
|
} else {
|
||||||
|
System.out.println("[SPMEGA] Card successfully synchronized with backend.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exceptionally(throwable -> {
|
||||||
|
System.err.println("[SPMEGA] Error sending card to backend: " + throwable.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<CardCredentials> fetchCardsFromBackend() throws IOException, InterruptedException {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null || !config.allowBackend()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiDomain = config.apiDomain();
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/auth/cards";
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new IOException("Server returned status code " + response.statusCode() + " on fetch cards.");
|
||||||
|
}
|
||||||
|
|
||||||
|
com.google.gson.JsonArray cardsArray = JsonParser.parseString(response.body()).getAsJsonArray();
|
||||||
|
List<CardCredentials> cards = new ArrayList<>();
|
||||||
|
for (com.google.gson.JsonElement el : cardsArray) {
|
||||||
|
JsonObject cardJson = el.getAsJsonObject();
|
||||||
|
String cardId = "";
|
||||||
|
if (cardJson.has("cardId") && !cardJson.get("cardId").isJsonNull()) {
|
||||||
|
cardId = cardJson.get("cardId").getAsString();
|
||||||
|
} else if (cardJson.has("id") && !cardJson.get("id").isJsonNull()) {
|
||||||
|
cardId = cardJson.get("id").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String cardToken = "";
|
||||||
|
if (cardJson.has("cardToken") && !cardJson.get("cardToken").isJsonNull()) {
|
||||||
|
cardToken = cardJson.get("cardToken").getAsString();
|
||||||
|
} else if (cardJson.has("token") && !cardJson.get("token").isJsonNull()) {
|
||||||
|
cardToken = cardJson.get("token").getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cardId.isEmpty() && !cardToken.isEmpty()) {
|
||||||
|
cards.add(new CardCredentials(cardId, cardToken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteCardOnBackend(String cardId) {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null || !config.allowBackend()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiDomain = config.apiDomain();
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/auth/cards/" + cardId;
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.DELETE()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
.thenAccept(response -> {
|
||||||
|
if (response.statusCode() != 200 && response.statusCode() != 204) {
|
||||||
|
System.err.println("[SPMEGA] Failed to delete card from backend: status code "
|
||||||
|
+ response.statusCode() + ", body: " + response.body());
|
||||||
|
} else {
|
||||||
|
System.out.println("[SPMEGA] Card successfully deleted from backend.");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exceptionally(throwable -> {
|
||||||
|
System.err.println("[SPMEGA] Error deleting card from backend: " + throwable.getMessage());
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean createTransactionOnBackend(String senderCardId, String receiver, long amount, String comment) throws IOException, InterruptedException {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null) {
|
||||||
|
throw new IOException("ModConfig is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiDomain = config.apiDomain();
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/transactions";
|
||||||
|
|
||||||
|
JsonObject jsonPayload = new JsonObject();
|
||||||
|
jsonPayload.addProperty("senderCardId", senderCardId);
|
||||||
|
jsonPayload.addProperty("cardId", senderCardId);
|
||||||
|
jsonPayload.addProperty("receiver", receiver);
|
||||||
|
jsonPayload.addProperty("recipient", receiver);
|
||||||
|
jsonPayload.addProperty("amount", amount);
|
||||||
|
jsonPayload.addProperty("comment", comment);
|
||||||
|
String requestBody = jsonPayload.toString();
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.PUT(HttpRequest.BodyPublishers.ofString(requestBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() == 200 || response.statusCode() == 201 || response.statusCode() == 204) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new IOException("Server returned status code " + response.statusCode() + ": " + response.body());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<BankDatabase.LocalTransaction> fetchTransactionsFromBackend(String cardId) throws IOException, InterruptedException {
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config == null || !config.allowBackend()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
String apiDomain = config.apiDomain();
|
||||||
|
if (apiDomain == null || apiDomain.isBlank()) {
|
||||||
|
apiDomain = ModConfig.DEFAULT_API_DOMAIN;
|
||||||
|
}
|
||||||
|
if (apiDomain.endsWith("/")) {
|
||||||
|
apiDomain = apiDomain.substring(0, apiDomain.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String url = apiDomain + "/api/v1/transactions?cardId=" + cardId;
|
||||||
|
|
||||||
|
HttpClient httpClient = HttpClient.newHttpClient();
|
||||||
|
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + config.apiToken())
|
||||||
|
.GET()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw new IOException("Server returned status code " + response.statusCode() + " on fetch transactions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
com.google.gson.JsonArray transactionsArray = JsonParser.parseString(response.body()).getAsJsonArray();
|
||||||
|
List<BankDatabase.LocalTransaction> list = new ArrayList<>();
|
||||||
|
for (com.google.gson.JsonElement el : transactionsArray) {
|
||||||
|
JsonObject json = el.getAsJsonObject();
|
||||||
|
String receiver = json.has("receiver") ? json.get("receiver").getAsString() : (json.has("recipient") ? json.get("recipient").getAsString() : "unknown");
|
||||||
|
long amount = json.has("amount") ? json.get("amount").getAsLong() : 0L;
|
||||||
|
String comment = json.has("comment") && !json.get("comment").isJsonNull() ? json.get("comment").getAsString() : "";
|
||||||
|
String status = json.has("status") ? json.get("status").getAsString() : "SUCCESS";
|
||||||
|
String createdAt = json.has("createdAt") ? json.get("createdAt").getAsString() : (json.has("created_at") ? json.get("created_at").getAsString() : "");
|
||||||
|
|
||||||
|
list.add(new BankDatabase.LocalTransaction(receiver, amount, comment, status, createdAt));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -166,6 +166,31 @@ public final class BankDatabase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized List<LocalTransaction> loadTransferHistory(String cardId) {
|
||||||
|
String sql = "SELECT receiver, amount, comment, status, created_at FROM transfer_history WHERE sender_card_id = ? ORDER BY id DESC";
|
||||||
|
List<LocalTransaction> result = new ArrayList<>();
|
||||||
|
try (Connection connection = open(); PreparedStatement statement = connection.prepareStatement(sql)) {
|
||||||
|
statement.setString(1, cardId);
|
||||||
|
try (ResultSet rs = statement.executeQuery()) {
|
||||||
|
while (rs.next()) {
|
||||||
|
result.add(new LocalTransaction(
|
||||||
|
rs.getString("receiver"),
|
||||||
|
rs.getLong("amount"),
|
||||||
|
rs.getString("comment"),
|
||||||
|
rs.getString("status"),
|
||||||
|
rs.getString("created_at")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (SQLException exception) {
|
||||||
|
throw new RuntimeException("Failed to load transfer history", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record LocalTransaction(String receiver, long amount, String comment, String status, String createdAt) {
|
||||||
|
}
|
||||||
|
|
||||||
private Connection open() throws SQLException {
|
private Connection open() throws SQLException {
|
||||||
return DriverManager.getConnection(jdbcUrl);
|
return DriverManager.getConnection(jdbcUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package git.yawaflua.tech.spmega.client.ui.service;
|
package git.yawaflua.tech.spmega.client.ui.service;
|
||||||
|
|
||||||
|
import git.yawaflua.tech.spmega.ModConfig;
|
||||||
|
import git.yawaflua.tech.spmega.SPMega;
|
||||||
import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
|
import git.yawaflua.tech.spmega.api.SPWorldsApiClient;
|
||||||
import net.fabricmc.loader.api.FabricLoader;
|
import net.fabricmc.loader.api.FabricLoader;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public final class BankUiService {
|
public final class BankUiService {
|
||||||
private static final BankUiService INSTANCE = new BankUiService();
|
private static final BankUiService INSTANCE = new BankUiService();
|
||||||
@@ -91,7 +94,28 @@ public final class BankUiService {
|
|||||||
return lastMessage;
|
return lastMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void syncCardsWithBackend(String playerUuid) {
|
||||||
|
try {
|
||||||
|
List<CardCredentials> backendCards = BackendAuthenticator.fetchCardsFromBackend();
|
||||||
|
boolean changed = false;
|
||||||
|
for (CardCredentials creds : backendCards) {
|
||||||
|
if (database.getCredentials(creds.cardId()) == null) {
|
||||||
|
database.upsertCardCredentials(creds.cardId(), creds.cardToken());
|
||||||
|
refreshCard(creds.cardId(), creds.cardToken(), playerUuid, false, false);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
reloadCardsFromDb();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[SPMEGA] Failed to sync cards with backend: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void refreshOnServerJoin(String playerUuid) {
|
public synchronized void refreshOnServerJoin(String playerUuid) {
|
||||||
|
syncCardsWithBackend(playerUuid);
|
||||||
|
|
||||||
List<StoredCard> storedCards = database.loadCards();
|
List<StoredCard> storedCards = database.loadCards();
|
||||||
for (StoredCard card : storedCards) {
|
for (StoredCard card : storedCards) {
|
||||||
refreshCard(card.cardId(), card.cardToken(), playerUuid, false, false);
|
refreshCard(card.cardId(), card.cardToken(), playerUuid, false, false);
|
||||||
@@ -110,7 +134,7 @@ public final class BankUiService {
|
|||||||
List<String> numbers = new ArrayList<>();
|
List<String> numbers = new ArrayList<>();
|
||||||
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
|
for (SPWorldsApiClient.PlayerCard apiCard : apiCards) {
|
||||||
if (apiCard.number() != null && !apiCard.number().isBlank()) {
|
if (apiCard.number() != null && !apiCard.number().isBlank()) {
|
||||||
numbers.add(apiCard.number());
|
numbers.add(apiCard.name() + " : " + apiCard.number());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastMessage = "";
|
lastMessage = "";
|
||||||
@@ -157,7 +181,12 @@ public final class BankUiService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
database.deleteCard(selected.id());
|
String cardId = selected.id();
|
||||||
|
database.deleteCard(cardId);
|
||||||
|
ModConfig config = SPMega.getConfig();
|
||||||
|
if (config.allowBackend())
|
||||||
|
BackendAuthenticator.deleteCardOnBackend(cardId);
|
||||||
|
|
||||||
reloadCardsFromDb();
|
reloadCardsFromDb();
|
||||||
if (selectedCardIndex >= cards.size()) {
|
if (selectedCardIndex >= cards.size()) {
|
||||||
selectedCardIndex = Math.max(0, cards.size() - 1);
|
selectedCardIndex = Math.max(0, cards.size() - 1);
|
||||||
@@ -189,21 +218,47 @@ public final class BankUiService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
||||||
|
boolean useBackend = config != null && config.allowBackend();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
long newBalance = 0;
|
||||||
|
if (useBackend) {
|
||||||
|
BackendAuthenticator.createTransactionOnBackend(
|
||||||
|
draft.senderCardId(),
|
||||||
|
draft.recipient(),
|
||||||
|
draft.amount(),
|
||||||
|
draft.comment()
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
refreshCard(credentials.cardId(), credentials.cardToken(), "", false, false);
|
||||||
|
StoredCard updatedCard = database.loadCards().stream()
|
||||||
|
.filter(c -> c.cardId().equals(credentials.cardId()))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
if (updatedCard != null) {
|
||||||
|
newBalance = updatedCard.balance();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("[SPMEGA] Failed to refresh card balance after transaction: " + e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
|
SPWorldsApiClient.TransactionResult result = apiClient.createTransaction(
|
||||||
toApiAuth(credentials),
|
toApiAuth(credentials),
|
||||||
draft.recipient(),
|
draft.recipient(),
|
||||||
draft.amount(),
|
draft.amount(),
|
||||||
draft.comment()
|
draft.comment()
|
||||||
);
|
);
|
||||||
|
newBalance = result.balance();
|
||||||
|
database.updateCardBalance(credentials.cardId(), newBalance);
|
||||||
|
}
|
||||||
|
|
||||||
database.updateCardBalance(credentials.cardId(), result.balance());
|
|
||||||
database.insertTransferHistory(
|
database.insertTransferHistory(
|
||||||
credentials.cardId(),
|
credentials.cardId(),
|
||||||
draft.recipient(),
|
draft.recipient(),
|
||||||
draft.amount(),
|
draft.amount(),
|
||||||
draft.comment(),
|
draft.comment(),
|
||||||
result.balance(),
|
newBalance,
|
||||||
"SUCCESS"
|
"SUCCESS"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -224,6 +279,10 @@ public final class BankUiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CompletableFuture<Boolean> submitPaymentAsync(PaymentDraft draft) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> submitPayment(draft));
|
||||||
|
}
|
||||||
|
|
||||||
private boolean refreshCard(
|
private boolean refreshCard(
|
||||||
String cardId,
|
String cardId,
|
||||||
String cardToken,
|
String cardToken,
|
||||||
@@ -259,6 +318,13 @@ public final class BankUiService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
git.yawaflua.tech.spmega.ModConfig config = git.yawaflua.tech.spmega.SPMega.getConfig();
|
||||||
|
if (config.allowBackend())
|
||||||
|
CompletableFuture.supplyAsync(() -> {
|
||||||
|
BackendAuthenticator.sendCardToBackend(cardId, cardToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
lastMessage = "";
|
lastMessage = "";
|
||||||
return true;
|
return true;
|
||||||
@@ -295,4 +361,8 @@ public final class BankUiService {
|
|||||||
}
|
}
|
||||||
return database.getCredentials(selected.id());
|
return database.getCredentials(selected.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BankDatabase getDatabase() {
|
||||||
|
return database;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,22 @@
|
|||||||
"message.spmega.qr.error": "Failed to scan QR from screen",
|
"message.spmega.qr.error": "Failed to scan QR from screen",
|
||||||
"message.spmega.qr.hover_tip": "Click to open link",
|
"message.spmega.qr.hover_tip": "Click to open link",
|
||||||
"message.spmega.qr.found_link": "Found link: %s",
|
"message.spmega.qr.found_link": "Found link: %s",
|
||||||
"message.spmega.qr.failed_open": "Failed to open link"
|
"message.spmega.qr.failed_open": "Failed to open link",
|
||||||
|
"key.spmega.toggle_gps": "Toggle Nether GPS",
|
||||||
|
"title.spmega.config": "SPMega Settings",
|
||||||
|
"category.spmega.general": "General Settings",
|
||||||
|
"option.spmega.api_domain": "API Domain",
|
||||||
|
"option.spmega.api_token": "API Token",
|
||||||
|
"option.spmega.allow_backend": "Allow saving data, transactions and other sensitive information to the backend",
|
||||||
|
"option.spmega.sign_quick_pay": "Quick Pay by Signs",
|
||||||
|
"option.spmega.gps_position": "Nether GPS position",
|
||||||
|
"option.spmega.gps_position.top_left": "Top-Left",
|
||||||
|
"option.spmega.gps_position.top_right": "Top-Right",
|
||||||
|
"option.spmega.gps_position.bottom_left": "Bottom-Left",
|
||||||
|
"option.spmega.gps_position.bottom_right": "Bottom-Right",
|
||||||
|
"option.spmega.gps_position.bottom_center": "Bottom-center",
|
||||||
|
"option.spmega.gps_position.top_center": "Top-center"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"category.spmega": "SPMega",
|
||||||
|
"key.spmega.open_menu": "Открыть меню SPMega",
|
||||||
|
"key.spmega.scan_qr": "Сканировать QR-код с экрана",
|
||||||
|
"key.spmega.toggle_gps": "Вкл/Выкл GPS Ада",
|
||||||
|
"button.spmega.scan_qr": "Сканировать QR",
|
||||||
|
"button.spmega.qr.cancel": "Отмена",
|
||||||
|
"button.spmega.qr.open_link": "Открыть ссылку",
|
||||||
|
"screen.spmega.qr.confirm_title": "Подтверждение ссылки",
|
||||||
|
"screen.spmega.qr.accept_link": "Открыть эту ссылку?",
|
||||||
|
"message.spmega.qr.capture_failed": "Не удалось сделать снимок экрана Minecraft",
|
||||||
|
"message.spmega.qr.not_found": "QR-код на экране не найден",
|
||||||
|
"message.spmega.qr.invalid_url": "QR-код не содержит валидный http/https URL",
|
||||||
|
"message.spmega.qr.opened": "Открыто: %s",
|
||||||
|
"message.spmega.qr.error": "Не удалось отсканировать QR-код с экрана",
|
||||||
|
"message.spmega.qr.hover_tip": "Нажмите, чтобы открыть ссылку",
|
||||||
|
"message.spmega.qr.found_link": "Найдена ссылка: %s",
|
||||||
|
"message.spmega.qr.failed_open": "Не удалось открыть ссылку",
|
||||||
|
"title.spmega.config": "Настройки SPMega",
|
||||||
|
"category.spmega.general": "Основные настройки",
|
||||||
|
"option.spmega.api_domain": "Домен API",
|
||||||
|
"option.spmega.api_token": "Токен API",
|
||||||
|
"option.spmega.allow_backend": "Разрешить сохранение данных, транзакций и другой получаемой информации на сервере",
|
||||||
|
"option.spmega.sign_quick_pay": "Быстрая оплата по табличкам",
|
||||||
|
"option.spmega.gps_position": "Положение GPS Ада",
|
||||||
|
"option.spmega.gps_position.top_left": "Слева вверху",
|
||||||
|
"option.spmega.gps_position.top_right": "Справа вверху",
|
||||||
|
"option.spmega.gps_position.bottom_left": "Слева внизу",
|
||||||
|
"option.spmega.gps_position.bottom_center": "Снизу по-центру",
|
||||||
|
"option.spmega.gps_position.top_center": "Сверху по-центру",
|
||||||
|
"option.spmega.gps_position.bottom_right": "Снизу справа"
|
||||||
|
}
|
||||||
|
|
||||||
@@ -42,19 +42,39 @@ public final class ConfigManager {
|
|||||||
shouldSave = true;
|
shouldSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean allowAccess = readBoolean(properties, "allow.backend", defaults.allowBackend());
|
||||||
|
String rawAllowAccess = properties.getProperty("sign.quickPay.enabled");
|
||||||
|
if (rawAllowAccess == null || !Boolean.toString(allowAccess).equalsIgnoreCase(rawAllowAccess.trim())) {
|
||||||
|
shouldSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
boolean signQuickPayEnabled = readBoolean(properties, "sign.quickPay.enabled", defaults.signQuickPayEnabled());
|
boolean signQuickPayEnabled = readBoolean(properties, "sign.quickPay.enabled", defaults.signQuickPayEnabled());
|
||||||
String rawQuickPay = properties.getProperty("sign.quickPay.enabled");
|
String rawQuickPay = properties.getProperty("sign.quickPay.enabled");
|
||||||
if (rawQuickPay == null || !Boolean.toString(signQuickPayEnabled).equalsIgnoreCase(rawQuickPay.trim())) {
|
if (rawQuickPay == null || !Boolean.toString(signQuickPayEnabled).equalsIgnoreCase(rawQuickPay.trim())) {
|
||||||
shouldSave = true;
|
shouldSave = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ModConfig config = new ModConfig(apiDomain, apiToken, signQuickPayEnabled);
|
boolean gpsEnabled = readBoolean(properties, "gps.enabled", defaults.gpsEnabled());
|
||||||
|
String rawGps = properties.getProperty("gps.enabled");
|
||||||
|
if (rawGps == null || !Boolean.toString(gpsEnabled).equalsIgnoreCase(rawGps.trim())) {
|
||||||
|
shouldSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
GpsHudPosition gpsPosition = readEnum(properties, "gps.position", GpsHudPosition.class, defaults.gpsPosition());
|
||||||
|
String rawPosition = properties.getProperty("gps.position");
|
||||||
|
if (rawPosition == null || !gpsPosition.name().equalsIgnoreCase(rawPosition.trim())) {
|
||||||
|
shouldSave = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ModConfig config = new ModConfig(apiDomain, apiToken, allowAccess, signQuickPayEnabled, gpsEnabled, gpsPosition);
|
||||||
|
|
||||||
|
|
||||||
if (shouldSave) {
|
if (shouldSave) {
|
||||||
save(configPath, config);
|
save(configPath, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String readString(Properties properties, String key, String fallback) {
|
private static String readString(Properties properties, String key, String fallback) {
|
||||||
@@ -97,11 +117,18 @@ public final class ConfigManager {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void save(ModConfig config) {
|
||||||
|
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(FILE_NAME);
|
||||||
|
save(configPath, config);
|
||||||
|
}
|
||||||
|
|
||||||
private static void save(Path configPath, ModConfig config) {
|
private static void save(Path configPath, ModConfig config) {
|
||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
properties.setProperty("api.domain", config.apiDomain());
|
properties.setProperty("api.domain", config.apiDomain());
|
||||||
properties.setProperty("api.token", config.apiToken());
|
properties.setProperty("api.token", config.apiToken());
|
||||||
properties.setProperty("sign.quickPay.enabled", Boolean.toString(config.signQuickPayEnabled()));
|
properties.setProperty("sign.quickPay.enabled", Boolean.toString(config.signQuickPayEnabled()));
|
||||||
|
properties.setProperty("gps.enabled", Boolean.toString(config.gpsEnabled()));
|
||||||
|
properties.setProperty("gps.position", config.gpsPosition().name());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Files.createDirectories(configPath.getParent());
|
Files.createDirectories(configPath.getParent());
|
||||||
@@ -112,5 +139,17 @@ public final class ConfigManager {
|
|||||||
throw new RuntimeException("Failed to save config: " + configPath, exception);
|
throw new RuntimeException("Failed to save config: " + configPath, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static <E extends Enum<E>> E readEnum(Properties properties, String key, Class<E> enumClass, E fallback) {
|
||||||
|
String value = properties.getProperty(key);
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Enum.valueOf(enumClass, value.trim().toUpperCase());
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package git.yawaflua.tech.spmega;
|
||||||
|
|
||||||
|
public enum GpsHudPosition {
|
||||||
|
TOP_LEFT("Слева вверху"),
|
||||||
|
TOP_RIGHT("Справа вверху"),
|
||||||
|
BOTTOM_LEFT("Слева внизу"),
|
||||||
|
BOTTOM_RIGHT("Справа внизу"),
|
||||||
|
|
||||||
|
TOP_CENTER("Сверху по-центру"),
|
||||||
|
BOTTOM_CENTER("Снизу по-центру");
|
||||||
|
|
||||||
|
private final String displayName;
|
||||||
|
|
||||||
|
GpsHudPosition(String displayName) {
|
||||||
|
this.displayName = displayName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDisplayName() {
|
||||||
|
return displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,24 @@
|
|||||||
package git.yawaflua.tech.spmega;
|
package git.yawaflua.tech.spmega;
|
||||||
|
|
||||||
public record ModConfig(String apiDomain, String apiToken, boolean signQuickPayEnabled) {
|
public record ModConfig(String apiDomain, String apiToken, boolean allowBackend, boolean signQuickPayEnabled,
|
||||||
public static final String DEFAULT_API_DOMAIN = "https://spmega-api.yawaflua.tech";
|
boolean gpsEnabled, GpsHudPosition gpsPosition) {
|
||||||
|
public static final String DEFAULT_API_DOMAIN = "http://localhost:5129";
|
||||||
|
public static final boolean ALLOW_BACKEND = false;
|
||||||
public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3";
|
public static final String DEFAULT_API_TOKEN = "ulBKE9MWEtIGiPAhXV69I28W9BRiSrV3";
|
||||||
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
public static final boolean DEFAULT_SIGN_QUICK_PAY_ENABLED = true;
|
||||||
|
public static final boolean DEFAULT_GPS_ENABLED = true;
|
||||||
|
public static final GpsHudPosition DEFAULT_GPS_POSITION = GpsHudPosition.TOP_LEFT;
|
||||||
|
|
||||||
public static ModConfig createDefault() {
|
public static ModConfig createDefault() {
|
||||||
return new ModConfig(
|
return new ModConfig(
|
||||||
DEFAULT_API_DOMAIN,
|
DEFAULT_API_DOMAIN,
|
||||||
DEFAULT_API_TOKEN,
|
DEFAULT_API_TOKEN,
|
||||||
DEFAULT_SIGN_QUICK_PAY_ENABLED
|
ALLOW_BACKEND,
|
||||||
|
DEFAULT_SIGN_QUICK_PAY_ENABLED,
|
||||||
|
DEFAULT_GPS_ENABLED,
|
||||||
|
DEFAULT_GPS_POSITION
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ public class SPMega implements ModInitializer {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void setConfig(ModConfig newConfig) {
|
||||||
|
config = newConfig;
|
||||||
|
ConfigManager.save(newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onInitialize() {
|
public void onInitialize() {
|
||||||
config = ConfigManager.loadOrCreate();
|
config = ConfigManager.loadOrCreate();
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ public final class SPWorldsApiClient {
|
|||||||
String body = send(request);
|
String body = send(request);
|
||||||
try {
|
try {
|
||||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||||
|
if (json.has("statusCode")) {
|
||||||
|
switch (json.get("statusCode").getAsInt()) {
|
||||||
|
case 403:
|
||||||
|
throw new IOException("Апи вернула ошибку: " + json.get("message").getAsString());
|
||||||
|
default:
|
||||||
|
System.out.println("Unhandled status code in card info response: " + json.get("statusCode").getAsInt());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
long balance = json.has("balance") ? json.get("balance").getAsLong() : 0L;
|
||||||
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
|
String webhook = json.has("webhook") && !json.get("webhook").isJsonNull()
|
||||||
? json.get("webhook").getAsString()
|
? json.get("webhook").getAsString()
|
||||||
|
|||||||
@@ -4,8 +4,12 @@
|
|||||||
"version": "${version}",
|
"version": "${version}",
|
||||||
"name": "SPMega",
|
"name": "SPMega",
|
||||||
"description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!",
|
"description": "yawaflua`s SPRadar+SPMHelper mod, thats make a lot!",
|
||||||
"authors": [],
|
"authors": [
|
||||||
"contact": {},
|
"Dmitri 'yawaflua' Shimanski aka DOLBAYEB"
|
||||||
|
],
|
||||||
|
"contact": {
|
||||||
|
|
||||||
|
},
|
||||||
"license": "CC BY-NC-ND 4.0",
|
"license": "CC BY-NC-ND 4.0",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"environment": "client",
|
"environment": "client",
|
||||||
@@ -16,6 +20,9 @@
|
|||||||
"client": [
|
"client": [
|
||||||
"git.yawaflua.tech.spmega.client.SPMegaClient"
|
"git.yawaflua.tech.spmega.client.SPMegaClient"
|
||||||
],
|
],
|
||||||
|
"modmenu": [
|
||||||
|
"git.yawaflua.tech.spmega.client.ModMenuIntegration"
|
||||||
|
],
|
||||||
"main": [
|
"main": [
|
||||||
"git.yawaflua.tech.spmega.SPMega"
|
"git.yawaflua.tech.spmega.SPMega"
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user