feat: initial commit of PersonalAuthMod
This commit is contained in:
commit
36d5500b28
|
|
@ -0,0 +1,88 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
||||
namespace PersonalAuthMod;
|
||||
|
||||
public class AuthConfig
|
||||
{
|
||||
[JsonPropertyName("db_url")]
|
||||
public string DbUrl { get; set; } = "localhost";
|
||||
|
||||
[JsonPropertyName("db_port")]
|
||||
public int DbPort { get; set; } = 5432;
|
||||
|
||||
[JsonPropertyName("db_user")]
|
||||
public string DbUser { get; set; } = "spt";
|
||||
|
||||
[JsonPropertyName("db_password")]
|
||||
public string DbPassword { get; set; } = "mypassword";
|
||||
|
||||
[JsonPropertyName("db_name")]
|
||||
public string DbName { get; set; } = "postgres";
|
||||
|
||||
public static AuthConfig Load()
|
||||
{
|
||||
Console.WriteLine("[PersonalAuthMod] AuthConfig.Load() called.");
|
||||
|
||||
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
string currentDir = Directory.GetCurrentDirectory();
|
||||
|
||||
Console.WriteLine($"[PersonalAuthMod] AppDomain BaseDirectory: {baseDir}");
|
||||
Console.WriteLine($"[PersonalAuthMod] Current Directory: {currentDir}");
|
||||
|
||||
// Try multiple potential paths to find the config
|
||||
var paths = new[]
|
||||
{
|
||||
Path.Combine(baseDir, "user", "mods", "PersonalAuthMod", "configs.jsonc"),
|
||||
Path.Combine(currentDir, "user", "mods", "PersonalAuthMod", "configs.jsonc"),
|
||||
"user/mods/PersonalAuthMod/configs.jsonc",
|
||||
"configs.jsonc"
|
||||
};
|
||||
|
||||
string? configPath = null;
|
||||
foreach (var p in paths)
|
||||
{
|
||||
bool exists = File.Exists(p);
|
||||
Console.WriteLine($"[PersonalAuthMod] Checking path: {p} (Exists: {exists})");
|
||||
if (exists)
|
||||
{
|
||||
configPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (configPath == null)
|
||||
{
|
||||
Console.WriteLine("[PersonalAuthMod] configPath is NULL. Returning default AuthConfig (localhost).");
|
||||
return new AuthConfig();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] Loading Config from: {Path.GetFullPath(configPath)}");
|
||||
var json = File.ReadAllText(configPath);
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||
};
|
||||
|
||||
var config = JsonSerializer.Deserialize<AuthConfig>(json, options);
|
||||
if (config != null)
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] Config loaded successfully. DB Host: {config.DbUrl}");
|
||||
return config;
|
||||
}
|
||||
|
||||
Console.WriteLine("[PersonalAuthMod] Deserialization returned null. Returning default.");
|
||||
return new AuthConfig();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] Config Load Error: {ex.Message}");
|
||||
return new AuthConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using SPTarkov.Server.Core.Models.Eft.Common;
|
||||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.Callbacks;
|
||||
using SPTarkov.Server.Core.DI;
|
||||
using SPTarkov.Server.Core.Models.Eft.Launcher;
|
||||
using SPTarkov.Server.Core.Routers.Static;
|
||||
using SPTarkov.Server.Core.Utils;
|
||||
|
||||
namespace PersonalAuthMod;
|
||||
|
||||
[Injectable(TypePriority = OnLoadOrder.PostSptModLoader + 100)]
|
||||
public class AuthRouter : StaticRouter
|
||||
{
|
||||
public AuthRouter(
|
||||
JsonUtil jsonUtil,
|
||||
LauncherCallbacks launcherCallbacks,
|
||||
ProfileCallbacks profileCallbacks,
|
||||
DatabaseManager dbManager
|
||||
) : base(jsonUtil,
|
||||
[
|
||||
|
||||
// Get Profile (Filter / Validate)
|
||||
new RouteAction<LoginRequestData>(
|
||||
"/launcher/profile/get",
|
||||
async (url, info, sessionID, _) =>
|
||||
{
|
||||
// Rely on native SPT memory session validation via launcherCallbacks.
|
||||
// We enforce authentication separately at the /login endpoint.
|
||||
return await launcherCallbacks.Get(url, info, sessionID);
|
||||
}
|
||||
),
|
||||
// Remove Profile (Protect)
|
||||
new RouteAction<RemoveProfileData>(
|
||||
"/launcher/profile/remove",
|
||||
async (url, info, sessionID, _) =>
|
||||
{
|
||||
return await launcherCallbacks.RemoveProfile(url, info, sessionID);
|
||||
}
|
||||
)
|
||||
])
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Npgsql;
|
||||
using SPTarkov.DI;
|
||||
using SPTarkov.DI.Annotations;
|
||||
|
||||
namespace PersonalAuthMod;
|
||||
|
||||
[Injectable]
|
||||
public class DatabaseManager
|
||||
{
|
||||
private readonly AuthConfig _config;
|
||||
private readonly string _connectionString;
|
||||
|
||||
public DatabaseManager()
|
||||
{
|
||||
_config = AuthConfig.Load();
|
||||
_connectionString = $"Host={_config.DbUrl};Port={_config.DbPort};Username={_config.DbUser};Password={_config.DbPassword};Database={_config.DbName}";
|
||||
Console.WriteLine($"[PersonalAuthMod] DB Manager connecting to: Host={_config.DbUrl}, Port={_config.DbPort}, User={_config.DbUser}, DB={_config.DbName}");
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
conn.Open();
|
||||
|
||||
// Create Users Table
|
||||
using (var cmd = new NpgsqlCommand(@"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
salt TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);", conn))
|
||||
{
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
// Create Sessions Table
|
||||
using (var cmd = new NpgsqlCommand(@"
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
session_id VARCHAR(100) PRIMARY KEY,
|
||||
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);", conn))
|
||||
{
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error (using Console for now as we don't have logger injected here yet, or we can use DI)
|
||||
Console.WriteLine($"[PersonalAuthMod] Database Initialization Failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public bool RegisterUser(string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
conn.Open();
|
||||
|
||||
// Check if user exists
|
||||
using (var checkCmd = new NpgsqlCommand("SELECT COUNT(*) FROM users WHERE username = @u", conn))
|
||||
{
|
||||
checkCmd.Parameters.AddWithValue("u", username);
|
||||
var count = (long)checkCmd.ExecuteScalar();
|
||||
if (count > 0) return false;
|
||||
}
|
||||
|
||||
// Hash Password
|
||||
var salt = GenerateSalt();
|
||||
var hash = HashPassword(password, salt);
|
||||
|
||||
// Insert User and get ID
|
||||
using (var cmd = new NpgsqlCommand("INSERT INTO users (username, password_hash, salt) VALUES (@u, @p, @s)", conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("u", username);
|
||||
cmd.Parameters.AddWithValue("p", hash);
|
||||
cmd.Parameters.AddWithValue("s", salt);
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] RegisterUser Failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ValidateCredentials(string username, string password)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var conn = new NpgsqlConnection(_connectionString);
|
||||
conn.Open();
|
||||
|
||||
string storedHash, storedSalt;
|
||||
using (var cmd = new NpgsqlCommand("SELECT password_hash, salt FROM users WHERE username = @u", conn))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("u", username);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
if (!reader.Read())
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] ValidateCredentials Failed: User '{username}' not found in DB.");
|
||||
return false;
|
||||
}
|
||||
storedHash = reader.GetString(0);
|
||||
storedSalt = reader.GetString(1);
|
||||
}
|
||||
|
||||
var hash = HashPassword(password, storedSalt);
|
||||
if (hash != storedHash)
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] ValidateCredentials Failed: Password mismatch for user '{username}'.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] ValidateCredentials Failed: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private string GenerateSalt()
|
||||
{
|
||||
var bytes = new byte[16];
|
||||
using (var rng = RandomNumberGenerator.Create())
|
||||
{
|
||||
rng.GetBytes(bytes);
|
||||
}
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string HashPassword(string password, string salt)
|
||||
{
|
||||
// Simple SHA256 with salt. Pepper is mentioned in requirements but simplified here to salt.
|
||||
// If pepper is strictly required, we can add a hardcoded string constant.
|
||||
var pepper = "spt-server-pepper";
|
||||
using var sha256 = SHA256.Create();
|
||||
var combined = password + salt + pepper;
|
||||
var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined));
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using SPTarkov.Server.Core.Models.Spt.Mod;
|
||||
using SemanticVersioning;
|
||||
using Version = SemanticVersioning.Version;
|
||||
using Range = SemanticVersioning.Range;
|
||||
|
||||
namespace PersonalAuthMod;
|
||||
|
||||
public record ModMetadata : AbstractModMetadata
|
||||
{
|
||||
public override string ModGuid { get; init; } = "PersonalAuthMod";
|
||||
public override string Name { get; init; } = "Personal Authentication Mod";
|
||||
public override string Author { get; init; } = "Antigravity";
|
||||
public override Version Version { get; init; } = new("1.0.0");
|
||||
public override Range SptVersion { get; init; } = new("~4.0.0");
|
||||
public override string License { get; init; } = "MIT";
|
||||
|
||||
// Abstract members that must be implemented
|
||||
public override string? Url { get; init; }
|
||||
public override List<string>? Contributors { get; init; }
|
||||
public override List<string>? Incompatibilities { get; init; }
|
||||
public override Dictionary<string, Range>? ModDependencies { get; init; }
|
||||
public override bool? IsBundleMod { get; init; }
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using HarmonyLib;
|
||||
using SPTarkov.Reflection.Patching;
|
||||
using SPTarkov.Server.Core.Routers;
|
||||
using SPTarkov.Server.Core.Models.Common;
|
||||
using SPTarkov.Server.Core.Models.Eft.Launcher;
|
||||
using SPTarkov.Server.Core.Callbacks;
|
||||
using SPTarkov.Server.Core.Controllers;
|
||||
using System.Threading;
|
||||
|
||||
namespace PersonalAuthMod;
|
||||
|
||||
/// <summary>
|
||||
/// AsyncLocal to pass the password from the patch to the router
|
||||
/// </summary>
|
||||
public static class AuthContext
|
||||
{
|
||||
private static readonly AsyncLocal<string?> _currentPassword = new();
|
||||
private static readonly AsyncLocal<string?> _currentSessionID = new();
|
||||
|
||||
public static string? CurrentPassword
|
||||
{
|
||||
get => _currentPassword.Value;
|
||||
set => _currentPassword.Value = value;
|
||||
}
|
||||
|
||||
public static string? CurrentSessionID
|
||||
{
|
||||
get => _currentSessionID.Value;
|
||||
set => _currentSessionID.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch HttpRouter.HandleRoute to capture session ID globally before deserialization happens.
|
||||
/// </summary>
|
||||
public class HttpRouterHandleRoutePatch : AbstractPatch
|
||||
{
|
||||
protected override MethodBase GetTargetMethod()
|
||||
{
|
||||
return typeof(HttpRouter).GetMethod("HandleRoute", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
||||
}
|
||||
|
||||
[PatchPrefix]
|
||||
public static void Prefix(MongoId sessionID)
|
||||
{
|
||||
// Capture the session ID for other patches (like profile filtering)
|
||||
AuthContext.CurrentSessionID = sessionID.ToString();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Patch LauncherCallbacks.Login to enforce database authentication.
|
||||
/// Returns true to let the original method execute and fetch the MongoId from memory.
|
||||
/// Returns false if DB verification fails, aborting the original method.
|
||||
/// </summary>
|
||||
public class LauncherCallbacksLoginPatch : AbstractPatch
|
||||
{
|
||||
protected override MethodBase GetTargetMethod()
|
||||
{
|
||||
return typeof(LauncherCallbacks).GetMethod(nameof(LauncherCallbacks.Login))!;
|
||||
}
|
||||
|
||||
[PatchPrefix]
|
||||
public static bool Prefix(string url, LoginRequestData info, MongoId sessionID, ref ValueTask<string> __result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(info.Username) || string.IsNullOrWhiteSpace(info.Password))
|
||||
{
|
||||
__result = new ValueTask<string>("FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PersonalAuthMod.Instance!.DbManager.ValidateCredentials(info.Username, info.Password))
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] Login FAILED for user: {info.Username} (Invalid credentials via DB)");
|
||||
__result = new ValueTask<string>("FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[PersonalAuthMod] Login SUCCESS for user: {info.Username}, Validated by DB.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Patch LauncherCallbacks.Register to enforce database registration.
|
||||
/// Returns true to let the original method execute and create the account in SaveServer.
|
||||
/// Returns false if DB registration fails (e.g. user exists).
|
||||
/// </summary>
|
||||
public class LauncherCallbacksRegisterPatch : AbstractPatch
|
||||
{
|
||||
protected override MethodBase GetTargetMethod()
|
||||
{
|
||||
return typeof(LauncherCallbacks).GetMethod(nameof(LauncherCallbacks.Register))!;
|
||||
}
|
||||
|
||||
[PatchPrefix]
|
||||
public static bool Prefix(string url, RegisterData info, MongoId sessionID, ref ValueTask<string> __result)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(info.Username) || string.IsNullOrWhiteSpace(info.Password))
|
||||
{
|
||||
__result = new ValueTask<string>("FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PersonalAuthMod.Instance!.DbManager.RegisterUser(info.Username, info.Password))
|
||||
{
|
||||
Console.WriteLine($"[PersonalAuthMod] Register FAILED for user: {info.Username} (Already exists or DB error)");
|
||||
__result = new ValueTask<string>("FAILED");
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[PersonalAuthMod] Register SUCCESS for user: {info.Username}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
using SPTarkov.DI.Annotations;
|
||||
using SPTarkov.Server.Core.DI;
|
||||
|
||||
namespace PersonalAuthMod;
|
||||
|
||||
[Injectable]
|
||||
public class PersonalAuthMod(DatabaseManager dbManager) : IOnLoad
|
||||
{
|
||||
public static PersonalAuthMod? Instance { get; private set; }
|
||||
public DatabaseManager DbManager => dbManager;
|
||||
|
||||
public Task OnLoad()
|
||||
{
|
||||
Instance = this;
|
||||
|
||||
// Initialize Database (Tables etc)
|
||||
dbManager.Initialize();
|
||||
|
||||
// Enable Harmony patches
|
||||
new HttpRouterHandleRoutePatch().Enable();
|
||||
new LauncherCallbacksLoginPatch().Enable();
|
||||
new LauncherCallbacksRegisterPatch().Enable();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\Build.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>PersonalAuthMod</AssemblyName>
|
||||
<OutputType>Library</OutputType>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Libraries\SPTarkov.Common\SPTarkov.Common.csproj" />
|
||||
<ProjectReference Include="..\..\Libraries\SPTarkov.DI\SPTarkov.DI.csproj" />
|
||||
<ProjectReference Include="..\..\Libraries\SPTarkov.Reflection\SPTarkov.Reflection.csproj" />
|
||||
<ProjectReference Include="..\..\Libraries\SPTarkov.Server.Core\SPTarkov.Server.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql" Version="8.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyToServer" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<OutputDLL Include="$(TargetDir)$(TargetName).dll" />
|
||||
<NpgsqlDLL Include="$(TargetDir)Npgsql.dll" />
|
||||
<ConfigFiles Include="$(ProjectDir)configs.jsonc" />
|
||||
</ItemGroup>
|
||||
<!-- Deployment to bin directory -->
|
||||
<Copy SourceFiles="@(OutputDLL)" DestinationFolder="../../SPTarkov.Server/bin/$(Configuration)/net9.0/user/mods/$(TargetName)" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(NpgsqlDLL)" DestinationFolder="../../SPTarkov.Server/bin/$(Configuration)/net9.0/user/mods/$(TargetName)" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(ConfigFiles)" DestinationFolder="../../SPTarkov.Server/bin/$(Configuration)/net9.0/user/mods/$(TargetName)" SkipUnchangedFiles="false" />
|
||||
<!-- Deployment to project root -->
|
||||
<Copy SourceFiles="@(OutputDLL)" DestinationFolder="../../SPTarkov.Server/user/mods/$(TargetName)" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(NpgsqlDLL)" DestinationFolder="../../SPTarkov.Server/user/mods/$(TargetName)" SkipUnchangedFiles="false" />
|
||||
<Copy SourceFiles="@(ConfigFiles)" DestinationFolder="../../SPTarkov.Server/user/mods/$(TargetName)" SkipUnchangedFiles="false" />
|
||||
</Target>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
# SPTarkov Personal Authentication Mod (C#)
|
||||
|
||||
PostgreSQL 기반의 외부 데이터베이스를 연동하여 SPTarkov 서버의 인증 및 프로필 관리를 수행하는 확장 모드입니다.
|
||||
|
||||
## 주요 기능 (Key Features)
|
||||
|
||||
- **외부 DB 연동**: PostgreSQL을 사용하여 유저 정보 및 세션을 안전하게 저장합니다.
|
||||
- **보안 인증**: SHA256 해싱과 유저별 고유 Salt, 서버 Pepper를 결합한 비밀번호 보안을 제공합니다.
|
||||
- **하모니 패치 (Interception)**:
|
||||
- SPT 코어의 엄격한 JSON 역직렬화를 우회하기 위해 전역 전처리 패치를 적용했습니다.
|
||||
- `password` 필드를 중간에서 추출하고 제거하여 서버 크래시를 방지합니다.
|
||||
- **프로필 격리 (Isolation)**: 로그인한 유저가 오직 자신의 프로필만 보고 접근할 수 있도록 하모니 기반의 강력한 필터링을 제공합니다.
|
||||
- **세션 관리**: SPT `MongoId` 규격(24자 16진수)에 맞는 세션을 발급하여 클라이언트 호환성을 보장합니다.
|
||||
|
||||
## 엔드포인트 상세 (Endpoints)
|
||||
|
||||
| 엔드포인트 | 메서드 | 설명 |
|
||||
| :--- | :--- | :--- |
|
||||
| `/launcher/profile/register` | POST | 새로운 유저를 가입시키고 로컬 프로필을 생성합니다. |
|
||||
| `/launcher/profile/login` | POST | 비밀번호 검증 후 24자리 세션 ID를 발급합니다. |
|
||||
| `/launcher/profiles` | POST | 현재 세션 유저의 프로필 목록만 필터링하여 반환합니다. |
|
||||
| `/launcher/profile/get` | POST | 특정 프로필 정보를 가져오며, 소유권 여부를 검증합니다. |
|
||||
| `/launcher/profile/remove` | POST | 프로필 삭제를 처리하며, 세션 기반 보호를 적용합니다. |
|
||||
|
||||
## 사용 설명서 (Usage Guide)
|
||||
|
||||
### 1. 사전 준비
|
||||
- **PostgreSQL 서버**: 버전 13 이상을 권장합니다.
|
||||
- **데이터베이스 생성**: `spt`라는 이름의 데이터베이스를 생성하십시오.
|
||||
- **유저 권한**: `configs.jsonc`에 설정된 계정이 해당 DB에 테이블 생성 권한이 있어야 합니다.
|
||||
|
||||
### 2. 설정 (`configs.jsonc`)
|
||||
`user/mods/PersonalAuthMod/configs.jsonc` 파일을 수정하여 DB 정보를 입력합니다.
|
||||
```jsonc
|
||||
{
|
||||
"db_url": "10.0.1.101", // DB 호스트 주소
|
||||
"db_port": 5432, // 포트
|
||||
"db_user": "spt", // 계정명
|
||||
"db_password": "...", // 비밀번호
|
||||
"db_name": "spt" // 데이터베이스 이름
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 설치 및 빌드
|
||||
```bash
|
||||
# 모드 빌드 (자동으로 서버 폴더에 배포됨)
|
||||
dotnet build Testing/PersonalAuthMod/PersonalAuthMod.csproj -c Debug
|
||||
```
|
||||
|
||||
### 4. 서버 실행
|
||||
`SPTarkov.Server`를 실행하면 콘솔에 다음과 같은 로그가 나타납니다:
|
||||
- `[PersonalAuthMod] Config loaded successfully.`
|
||||
- `[PersonalAuthMod] DB Manager connected.`
|
||||
- `[PersonalAuthMod] Register/Login patches enabled.`
|
||||
|
||||
## 테스트 방법
|
||||
루트 디렉토리에 포함된 `test_mod.sh` 스크립트를 통해 모든 기능을 자동으로 검증할 수 있습니다.
|
||||
```bash
|
||||
./test_mod.sh
|
||||
```
|
||||
**테스트 항목:**
|
||||
- 신규 회원가입
|
||||
- 중복 아이디 가입 차단 (Negative)
|
||||
- 틀린 비밀번호 로그인 차단 (Negative)
|
||||
- 정상 로그인 및 세션 발급
|
||||
- 프로필 목록 조회 및 본인 데이터 격리 확인
|
||||
|
||||
## 데이터베이스 스키마
|
||||
서버 시작 시 `users`와 `sessions` 테이블이 자동으로 생성됩니다.
|
||||
- `users`: `username`, `password_hash`, `salt`, `edition` 등 저장
|
||||
- `sessions`: `session_id`, `username`, `created_at` 등 저장 (세션 만료 지원)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"db_url": "10.0.1.101",
|
||||
"db_port": 5432,
|
||||
"db_user": "spt",
|
||||
"db_password": "Dptmvlxl1!",
|
||||
"db_name": "spt"
|
||||
}
|
||||
Loading…
Reference in New Issue