API V1 Aeldria

This commit is contained in:
2026-06-23 13:32:17 +02:00
commit b56c82d229
164 changed files with 5666 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.17" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
@Aeldria.Api_HostAddress = http://localhost:5152
GET {{Aeldria.Api_HostAddress}}/weatherforecast/
Accept: application/json
###
+76
View File
@@ -0,0 +1,76 @@
using BCrypt.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Aeldria.Api.Data;
using Aeldria.Api.DTOs;
using Aeldria.Api.Models;
using Aeldria.Api.Services;
namespace Aeldria.Api.Controllers;
[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
private readonly AeldriaDbContext _db;
private readonly JwtService _jwt;
public AuthController(AeldriaDbContext db, JwtService jwt)
{
_db = db;
_jwt = jwt;
}
[HttpPost("register")]
public async Task<IActionResult> Register(RegisterRequest request)
{
if (await _db.Accounts.AnyAsync(x => x.Username == request.Username))
return BadRequest("Nom d'utilisateur déjà utilisé.");
if (await _db.Accounts.AnyAsync(x => x.Email == request.Email))
return BadRequest("Email déjà utilisé.");
var account = new Account
{
Username = request.Username,
Email = request.Email,
PasswordHash = PasswordService.HashPassword(request.Password),
CreatedAt = DateTime.UtcNow,
IsBanned = false,
IsVerified = false
};
_db.Accounts.Add(account);
await _db.SaveChangesAsync();
return Ok(new
{
Message = "Compte créé avec succès.",
AccountId = account.AccountId
});
}
[HttpPost("login")]
public async Task<IActionResult> Login(LoginRequest request)
{
var account = await _db.Accounts
.FirstOrDefaultAsync(x => x.Username == request.Username);
if (account == null)
return Unauthorized("Compte introuvable.");
if (!PasswordService.VerifyPassword(request.Password, account.PasswordHash))
return Unauthorized("Mot de passe incorrect.");
account.LastLogin = DateTime.UtcNow;
await _db.SaveChangesAsync();
var token = _jwt.GenerateToken(account);
return Ok(new
{
Message = "Connexion réussie.",
AccountId = account.AccountId,
Username = account.Username,
Token = token
});
}
}
+98
View File
@@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Aeldria.Api.Data;
using Aeldria.Api.Models;
using Aeldria.Api.DTOs;
namespace Aeldria.Api.Controllers;
[ApiController]
[Authorize]
[Route("api/[controller]")]
public class CharactersController : ControllerBase
{
private readonly AeldriaDbContext _db;
public CharactersController(AeldriaDbContext db)
{
_db = db;
}
[HttpPost("create")]
public async Task<IActionResult> Create(CreateCharacterRequest request)
{
if (await _db.Characters.AnyAsync(x => x.Name == request.Name))
return BadRequest("Nom déjà utilisé.");
var accountId = long.Parse(
User.FindFirst(
System.Security.Claims.ClaimTypes.NameIdentifier
)!.Value
);
var character = new Character
{
AccountId = accountId,
Name = request.Name,
Level = 1,
Experience = 0,
PosX = 0,
PosY = 0,
PosZ = 0,
CreatedAt = DateTime.UtcNow,
FactionId = 1,
RaceId = 1
};
_db.Characters.Add(character);
await _db.SaveChangesAsync();
return Ok(character);
}
[HttpGet("my")]
public async Task<IActionResult> GetCharacters()
{
var accountId = long.Parse(
User.FindFirst(
System.Security.Claims.ClaimTypes.NameIdentifier
)!.Value
);
var characters = await _db.Characters
.Where(x => x.AccountId == accountId)
.ToListAsync();
return Ok(characters);
}
[HttpGet("{characterId}")]
public async Task<IActionResult> GetCharacter(long characterId)
{
var character = await _db.Characters
.FirstOrDefaultAsync(x => x.CharacterId == characterId);
if (character == null)
return NotFound();
return Ok(character);
}
[HttpPut("position")]
public async Task<IActionResult> UpdatePosition(UpdatePositionRequest request)
{
var character = await _db.Characters
.FirstOrDefaultAsync(x => x.CharacterId == request.CharacterId);
if (character == null)
return NotFound();
character.PosX = request.PosX;
character.PosY = request.PosY;
character.PosZ = request.PosZ;
await _db.SaveChangesAsync();
return Ok(new
{
Message = "Position sauvegardée."
});
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace Aeldria.Api.DTOs;
public class CreateCharacterRequest
{
public string Name { get; set; } = string.Empty;
}
+8
View File
@@ -0,0 +1,8 @@
namespace Aeldria.Api.DTOs;
public class LoginRequest
{
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
+10
View File
@@ -0,0 +1,10 @@
namespace Aeldria.Api.DTOs;
public class RegisterRequest
{
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
+12
View File
@@ -0,0 +1,12 @@
namespace Aeldria.Api.DTOs;
public class UpdatePositionRequest
{
public long CharacterId { get; set; }
public double PosX { get; set; }
public double PosY { get; set; }
public double PosZ { get; set; }
}
+93
View File
@@ -0,0 +1,93 @@
using Microsoft.EntityFrameworkCore;
using Aeldria.Api.Models;
namespace Aeldria.Api.Data;
public class AeldriaDbContext : DbContext
{
public AeldriaDbContext(DbContextOptions<AeldriaDbContext> options)
: base(options)
{
}
public DbSet<Account> Accounts => Set<Account>();
public DbSet<Character> Characters => Set<Character>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema("aeldria");
modelBuilder.Entity<Account>(entity =>
{
entity.ToTable("accounts");
entity.HasKey(e => e.AccountId);
entity.Property(e => e.AccountId)
.HasColumnName("account_id");
entity.Property(e => e.Username)
.HasColumnName("username");
entity.Property(e => e.Email)
.HasColumnName("email");
entity.Property(e => e.PasswordHash)
.HasColumnName("password_hash");
entity.Property(e => e.CreatedAt)
.HasColumnName("created_at");
entity.Property(e => e.LastLogin)
.HasColumnName("last_login");
entity.Property(e => e.IsBanned)
.HasColumnName("is_banned");
entity.Property(e => e.IsVerified)
.HasColumnName("is_verified");
});
modelBuilder.Entity<Character>(entity =>
{
entity.ToTable("characters");
entity.HasKey(e => e.CharacterId);
entity.Property(e => e.CharacterId)
.HasColumnName("character_id");
entity.Property(e => e.AccountId)
.HasColumnName("account_id");
entity.Property(e => e.FactionId)
.HasColumnName("faction_id");
entity.Property(e => e.RaceId)
.HasColumnName("race_id");
entity.Property(e => e.Name)
.HasColumnName("name");
entity.Property(e => e.Level)
.HasColumnName("level");
entity.Property(e => e.Experience)
.HasColumnName("experience");
entity.Property(e => e.PosX)
.HasColumnName("pos_x");
entity.Property(e => e.PosY)
.HasColumnName("pos_y");
entity.Property(e => e.PosZ)
.HasColumnName("pos_z");
entity.Property(e => e.CreatedAt)
.HasColumnName("created_at");
});
base.OnModelCreating(modelBuilder);
}
}
+84
View File
@@ -0,0 +1,84 @@
// <auto-generated />
using System;
using Aeldria.Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Aeldria.Api.Migrations
{
[DbContext(typeof(AeldriaDbContext))]
[Migration("20260623072757_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Aeldria.Api.Models.Account", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Accounts");
});
modelBuilder.Entity("Aeldria.Api.Models.Character", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AccountId")
.HasColumnType("integer");
b.Property<int>("Experience")
.HasColumnType("integer");
b.Property<int>("Level")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Characters");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,58 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Aeldria.Api.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Accounts",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Username = table.Column<string>(type: "text", nullable: false),
Email = table.Column<string>(type: "text", nullable: false),
PasswordHash = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Accounts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Characters",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
AccountId = table.Column<int>(type: "integer", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Level = table.Column<int>(type: "integer", nullable: false),
Experience = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Characters", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Accounts");
migrationBuilder.DropTable(
name: "Characters");
}
}
}
@@ -0,0 +1,81 @@
// <auto-generated />
using System;
using Aeldria.Api.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Aeldria.Api.Migrations
{
[DbContext(typeof(AeldriaDbContext))]
partial class AeldriaDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.6")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Aeldria.Api.Models.Account", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Accounts");
});
modelBuilder.Entity("Aeldria.Api.Models.Character", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AccountId")
.HasColumnType("integer");
b.Property<int>("Experience")
.HasColumnType("integer");
b.Property<int>("Level")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Characters");
});
#pragma warning restore 612, 618
}
}
}
+20
View File
@@ -0,0 +1,20 @@
namespace Aeldria.Api.Models;
public class Account
{
public long AccountId { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public DateTime? LastLogin { get; set; }
public bool IsBanned { get; set; }
public bool IsVerified { get; set; }
}
+26
View File
@@ -0,0 +1,26 @@
namespace Aeldria.Api.Models;
public class Character
{
public long CharacterId { get; set; }
public long AccountId { get; set; }
public int FactionId { get; set; }
public int RaceId { get; set; }
public string Name { get; set; } = string.Empty;
public int Level { get; set; }
public long Experience { get; set; }
public double PosX { get; set; }
public double PosY { get; set; }
public double PosZ { get; set; }
public DateTime CreatedAt { get; set; }
}
+91
View File
@@ -0,0 +1,91 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Aeldria.Api.Services;
using Microsoft.EntityFrameworkCore;
using Aeldria.Api.Data;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedProto;
});
builder.Services.AddDbContext<AeldriaDbContext>(options =>
options.UseNpgsql(
builder.Configuration.GetConnectionString("DefaultConnection")));
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddScoped<JwtService>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(
builder.Configuration["Jwt:Key"]!
))
};
});
builder.Services.AddAuthorization();
builder.Services.AddOpenApi();
builder.Services.AddControllers();
var app = builder.Build();
app.UseForwardedHeaders();
app.UseAuthentication();
app.UseAuthorization();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
//app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");
app.MapControllers();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
+23
View File
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://0.0.0.0:5152",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://0.0.0.0:7241;http://0.0.0.0:5152",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.IdentityModel.Tokens;
using Aeldria.Api.Models;
namespace Aeldria.Api.Services;
public class JwtService
{
private readonly IConfiguration _config;
public JwtService(IConfiguration config)
{
_config = config;
}
public string GenerateToken(Account account)
{
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, account.AccountId.ToString()),
new Claim(ClaimTypes.Name, account.Username)
};
var key = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_config["Jwt:Key"]!)
);
var creds = new SigningCredentials(
key,
SecurityAlgorithms.HmacSha256
);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddDays(7),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace Aeldria.Api.Services;
public static class PasswordService
{
public static string HashPassword(string password)
{
return BCrypt.Net.BCrypt.HashPassword(password);
}
public static bool VerifyPassword(string password, string hash)
{
return BCrypt.Net.BCrypt.Verify(password, hash);
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.1.165;Port=5432;Database=aeldria;Username=aeldria;Password=Aeldria@667-Ekip"
},
"Jwt": {
"Key": "AeldriaSuperSecretKey2026Minimum32Characters",
"Issuer": "Aeldria.Api",
"Audience": "Aeldria.Client"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
BIN
View File
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
{
"runtimeOptions": {
"tfm": "net9.0",
"frameworks": [
{
"name": "Microsoft.NETCore.App",
"version": "9.0.0"
},
{
"name": "Microsoft.AspNetCore.App",
"version": "9.0.0"
}
],
"configProperties": {
"System.GC.Server": true,
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}
@@ -0,0 +1,5 @@
{
"Version": 1,
"ManifestType": "Build",
"Endpoints": []
}
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+17
View File
@@ -0,0 +1,17 @@
{
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.1.165;Port=5432;Database=aeldria;Username=aeldria;Password=Aeldria@667-Ekip"
},
"Jwt": {
"Key": "AeldriaSuperSecretKey2026Minimum32Characters",
"Issuer": "Aeldria.Api",
"Audience": "Aeldria.Client"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More