feat: Project peready 2.0

This commit is contained in:
ereshk1gal
2025-10-01 01:42:45 +03:00
parent 504f03bd32
commit ece9cedb37
9 changed files with 1270 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
namespace LctMonolith.Application.Options;
public class S3StorageOptions
{
public string Endpoint { get; set; } = string.Empty;
public bool UseSsl { get; set; } = true;
public string AccessKey { get; set; } = string.Empty;
public string SecretKey { get; set; } = string.Empty;
public string Bucket { get; set; } = "avatars";
public string? PublicBaseUrl { get; set; } // optional CDN / reverse proxy base
public int PresignExpirationMinutes { get; set; } = 60;
}

View File

@@ -0,0 +1,67 @@
using System.Security.Claims;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace LctMonolith.Controllers;
[ApiController]
[Route("api/profile")]
[Authorize]
public class ProfileController : ControllerBase
{
private readonly IProfileService _profiles;
public ProfileController(IProfileService profiles) => _profiles = profiles;
private Guid CurrentUserId() => Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!);
public class UpdateProfileDto
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public string? About { get; set; }
public string? Location { get; set; }
}
[HttpGet("me")]
public async Task<IActionResult> GetMe(CancellationToken ct)
{
var p = await _profiles.GetByUserIdAsync(CurrentUserId(), ct);
if (p == null) return NotFound();
return Ok(p);
}
[HttpGet("{userId:guid}")]
[Authorize(Roles = "Admin")]
public async Task<IActionResult> GetByUser(Guid userId, CancellationToken ct)
{
var p = await _profiles.GetByUserIdAsync(userId, ct);
return p == null ? NotFound() : Ok(p);
}
[HttpPut]
public async Task<IActionResult> Upsert(UpdateProfileDto dto, CancellationToken ct)
{
var p = await _profiles.UpsertAsync(CurrentUserId(), dto.FirstName, dto.LastName, dto.BirthDate, dto.About, dto.Location, ct);
return Ok(p);
}
[HttpPost("avatar")]
[RequestSizeLimit(7_000_000)] // ~7MB
public async Task<IActionResult> UploadAvatar(IFormFile file, CancellationToken ct)
{
if (file == null || file.Length == 0) return BadRequest("File required");
await using var stream = file.OpenReadStream();
var p = await _profiles.UpdateAvatarAsync(CurrentUserId(), stream, file.ContentType, file.FileName, ct);
return Ok(new { p.AvatarUrl });
}
[HttpDelete("avatar")]
public async Task<IActionResult> DeleteAvatar(CancellationToken ct)
{
var ok = await _profiles.DeleteAvatarAsync(CurrentUserId(), ct);
return ok ? NoContent() : NotFound();
}
}

View File

@@ -31,6 +31,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
<!-- Updated to match transitive requirement (>=8.0.1) -->
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.0.1" />
<PackageReference Include="AWSSDK.S3" Version="3.7.400.6" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,21 @@
namespace LctMonolith.Models.Database;
public class Profile
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public AppUser User { get; set; } = null!;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public DateOnly? BirthDate { get; set; }
public string? About { get; set; }
public string? Location { get; set; }
// Avatar in S3 / MinIO
public string? AvatarS3Key { get; set; }
public string? AvatarUrl { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}

View File

@@ -0,0 +1,8 @@
namespace LctMonolith.Services.Interfaces;
public interface IFileStorageService
{
Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default);
Task DeleteAsync(string key, CancellationToken ct = default);
Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default);
}

View File

@@ -0,0 +1,11 @@
namespace LctMonolith.Services.Interfaces;
using LctMonolith.Models.Database;
public interface IProfileService
{
Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default);
Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default);
Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default);
Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default);
}

View File

@@ -0,0 +1,121 @@
using LctMonolith.Database.UnitOfWork;
using LctMonolith.Models.Database;
using LctMonolith.Services.Interfaces;
using Serilog;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace LctMonolith.Services;
public class ProfileService : IProfileService
{
private readonly IUnitOfWork _uow;
private readonly IFileStorageService _storage;
public ProfileService(IUnitOfWork uow, IFileStorageService storage)
{
_uow = uow;
_storage = storage;
}
public async Task<Profile?> GetByUserIdAsync(Guid userId, CancellationToken ct = default)
{
try
{
return await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
}
catch (Exception ex)
{
Log.Error(ex, "Profile get failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpsertAsync(Guid userId, string? firstName, string? lastName, DateOnly? birthDate, string? about, string? location, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null)
{
profile = new Profile
{
Id = Guid.NewGuid(),
UserId = userId,
FirstName = firstName,
LastName = lastName,
BirthDate = birthDate,
About = about,
Location = location
};
await _uow.Profiles.AddAsync(profile, ct);
}
else
{
profile.FirstName = firstName;
profile.LastName = lastName;
profile.BirthDate = birthDate;
profile.About = about;
profile.Location = location;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
}
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Profile upsert failed {UserId}", userId);
throw;
}
}
public async Task<Profile> UpdateAvatarAsync(Guid userId, Stream fileStream, string contentType, string? fileName, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct) ??
await UpsertAsync(userId, null, null, null, null, null, ct);
// Delete old if exists
if (!string.IsNullOrWhiteSpace(profile.AvatarS3Key))
{
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
}
var key = await _storage.UploadAsync(fileStream, contentType, $"avatars/{userId}", ct);
var url = await _storage.GetPresignedUrlAsync(key, TimeSpan.FromHours(6), ct);
profile.AvatarS3Key = key;
profile.AvatarUrl = url;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return profile;
}
catch (Exception ex)
{
Log.Error(ex, "Avatar update failed {UserId}", userId);
throw;
}
}
public async Task<bool> DeleteAvatarAsync(Guid userId, CancellationToken ct = default)
{
try
{
var profile = await _uow.Profiles.Query(p => p.UserId == userId).FirstOrDefaultAsync(ct);
if (profile == null || string.IsNullOrWhiteSpace(profile.AvatarS3Key)) return false;
await _storage.DeleteAsync(profile.AvatarS3Key!, ct);
profile.AvatarS3Key = null;
profile.AvatarUrl = null;
profile.UpdatedAt = DateTime.UtcNow;
_uow.Profiles.Update(profile);
await _uow.SaveChangesAsync(ct);
return true;
}
catch (Exception ex)
{
Log.Error(ex, "Delete avatar failed {UserId}", userId);
throw;
}
}
}

View File

@@ -0,0 +1,102 @@
using Amazon.S3;
using Amazon.S3.Model;
using Amazon;
using Microsoft.Extensions.Options;
using LctMonolith.Application.Options;
using LctMonolith.Services.Interfaces;
using Serilog;
namespace LctMonolith.Services;
public class S3FileStorageService : IFileStorageService, IDisposable
{
private readonly S3StorageOptions _opts;
private readonly IAmazonS3 _client;
private bool _bucketChecked;
public S3FileStorageService(IOptions<S3StorageOptions> options)
{
_opts = options.Value;
var cfg = new AmazonS3Config
{
ServiceURL = _opts.Endpoint,
ForcePathStyle = true,
UseHttp = !_opts.UseSsl,
Timeout = TimeSpan.FromSeconds(30),
MaxErrorRetry = 2,
};
_client = new AmazonS3Client(_opts.AccessKey, _opts.SecretKey, cfg);
}
private async Task EnsureBucketAsync(CancellationToken ct)
{
if (_bucketChecked) return;
try
{
var list = await _client.ListBucketsAsync(ct);
if (!list.Buckets.Any(b => string.Equals(b.BucketName, _opts.Bucket, StringComparison.OrdinalIgnoreCase)))
{
await _client.PutBucketAsync(new PutBucketRequest { BucketName = _opts.Bucket }, ct);
}
_bucketChecked = true;
}
catch (Exception ex)
{
Log.Error(ex, "Failed ensuring bucket {Bucket}", _opts.Bucket);
throw;
}
}
public async Task<string> UploadAsync(Stream content, string contentType, string keyPrefix, CancellationToken ct = default)
{
await EnsureBucketAsync(ct);
var key = $"{keyPrefix.Trim('/')}/{DateTime.UtcNow:yyyyMMdd}/{Guid.NewGuid():N}";
var putReq = new PutObjectRequest
{
BucketName = _opts.Bucket,
Key = key,
InputStream = content,
ContentType = contentType
};
await _client.PutObjectAsync(putReq, ct);
Log.Information("Uploaded object {Key} to bucket {Bucket}", key, _opts.Bucket);
return key;
}
public async Task DeleteAsync(string key, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(key)) return;
try
{
await _client.DeleteObjectAsync(_opts.Bucket, key, ct);
Log.Information("Deleted object {Key}", key);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
// ignore
}
}
public Task<string> GetPresignedUrlAsync(string key, TimeSpan? expires = null, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
if (!string.IsNullOrWhiteSpace(_opts.PublicBaseUrl))
{
var url = _opts.PublicBaseUrl!.TrimEnd('/') + "/" + key;
return Task.FromResult(url);
}
var req = new GetPreSignedUrlRequest
{
BucketName = _opts.Bucket,
Key = key,
Expires = DateTime.UtcNow.Add(expires ?? TimeSpan.FromMinutes(_opts.PresignExpirationMinutes))
};
var urlSigned = _client.GetPreSignedURL(req);
return Task.FromResult(urlSigned);
}
public void Dispose()
{
_client.Dispose();
}
}

View File

@@ -0,0 +1,927 @@
openapi: 3.0.3
info:
title: LctMonolith Gamification API
version: 1.0.0
description: |
Comprehensive REST API for gamification module (players, missions, ranks, skills, rewards, dialogue, inventory, store, analytics, notifications, auth).
Authentication via JWT Bearer (Authorization: Bearer <token>). Admin endpoints require role=Admin.
servers:
- url: https://localhost:5001
description: Local HTTPS
- url: http://localhost:5000
description: Local HTTP
security:
- bearerAuth: []
tags:
- name: Auth
- name: Players
- name: Ranks
- name: Skills
- name: MissionCategories
- name: Missions
- name: Rewards
- name: Dialogue
- name: Inventory
- name: Store
- name: Notifications
- name: Analytics
- name: Profile
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
Id: { type: string, format: uuid }
TokenPair:
type: object
properties:
accessToken: { type: string }
refreshToken: { type: string }
expiresAt: { type: string, format: date-time }
refreshExpiresAt: { type: string, format: date-time, nullable: true }
Rank:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
title: { type: string }
expNeeded: { type: integer }
Skill:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
title: { type: string }
Player:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
userId: { $ref: '#/components/schemas/Id' }
rankId: { $ref: '#/components/schemas/Id' }
experience: { type: integer }
mana: { type: integer }
PlayerSkill:
type: object
properties:
playerId: { $ref: '#/components/schemas/Id' }
skillId: { $ref: '#/components/schemas/Id' }
score: { type: integer }
Mission:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
title: { type: string }
description: { type: string }
missionCategoryId: { $ref: '#/components/schemas/Id' }
parentMissionId: { $ref: '#/components/schemas/Id', nullable: true }
expReward: { type: integer }
manaReward: { type: integer }
MissionSkillReward:
type: object
properties:
missionId: { $ref: '#/components/schemas/Id' }
skillId: { $ref: '#/components/schemas/Id' }
value: { type: integer }
MissionItemReward:
type: object
properties:
missionId: { $ref: '#/components/schemas/Id' }
itemId: { $ref: '#/components/schemas/Id' }
SkillProgress:
type: object
properties:
skillId: { $ref: '#/components/schemas/Id' }
skillTitle: { type: string }
previousLevel: { type: integer }
newLevel: { type: integer }
MissionCompletionResult:
type: object
properties:
success: { type: boolean }
message: { type: string }
experienceGained: { type: integer }
manaGained: { type: integer }
skillsProgress: { type: array, items: { $ref: '#/components/schemas/SkillProgress' } }
unlockedMissions: { type: array, items: { $ref: '#/components/schemas/Id' } }
PlayerProgress:
type: object
properties:
playerId: { $ref: '#/components/schemas/Id' }
playerName: { type: string }
currentRank: { $ref: '#/components/schemas/Rank', nullable: true }
totalExperience: { type: integer }
totalMana: { type: integer }
completedMissions: { type: integer }
totalAvailableMissions: { type: integer }
skillLevels:
type: object
additionalProperties: { type: integer }
StoreItem:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
name: { type: string }
description: { type: string, nullable: true }
price: { type: integer }
isActive: { type: boolean }
stock: { type: integer, nullable: true }
UserInventoryItem:
type: object
properties:
userId: { $ref: '#/components/schemas/Id' }
storeItemId: { $ref: '#/components/schemas/Id' }
quantity: { type: integer }
acquiredAt: { type: string, format: date-time }
Notification:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
type: { type: string }
title: { type: string }
message: { type: string }
isRead: { type: boolean }
createdAt: { type: string, format: date-time }
readAt: { type: string, format: date-time, nullable: true }
AnalyticsSummary:
type: object
properties:
totalUsers: { type: integer }
totalMissions: { type: integer }
totalStoreItems: { type: integer }
totalExperience: { type: integer }
completedMissions: { type: integer }
generatedAtUtc: { type: string, format: date-time }
Profile:
type: object
properties:
id: { $ref: '#/components/schemas/Id' }
userId: { $ref: '#/components/schemas/Id' }
firstName: { type: string, nullable: true }
lastName: { type: string, nullable: true }
birthDate: { type: string, format: date, nullable: true }
about: { type: string, nullable: true }
location: { type: string, nullable: true }
avatarUrl: { type: string, nullable: true }
createdAt: { type: string, format: date-time }
updatedAt: { type: string, format: date-time }
paths:
/api/auth/register:
post:
tags: [Auth]
summary: Register user
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email: { type: string, format: email }
password: { type: string }
firstName: { type: string }
lastName: { type: string }
required: [email,password]
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } }
/api/auth/login:
post:
tags: [Auth]
summary: Login
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
email: { type: string, format: email }
password: { type: string }
required: [email,password]
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } }
/api/auth/refresh:
post:
tags: [Auth]
summary: Refresh access token
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
refreshToken: { type: string }
required: [refreshToken]
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/TokenPair' } } } }
/api/auth/revoke:
post:
tags: [Auth]
security: [ { bearerAuth: [] } ]
summary: Revoke refresh token
requestBody:
content:
application/json:
schema:
type: object
properties:
refreshToken: { type: string }
responses:
'204': { description: No Content }
/api/auth/me:
get:
tags: [Auth]
security: [ { bearerAuth: [] } ]
summary: Current user id
responses:
'200': { description: OK }
/api/analytics/summary:
get:
tags: [Analytics]
security: [ { bearerAuth: [] } ]
summary: Aggregated analytics summary
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/AnalyticsSummary' } } } }
/api/players/{playerId}:
get:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Get player by id
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses:
'200': { description: OK }
'404': { description: Not Found }
/api/players/{playerId}/progress:
get:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Get player overall progress
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/PlayerProgress' } } } }
'404': { description: Not Found }
/api/players/user/{userId}:
get:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Get player by user id
parameters:
- in: path
name: userId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses:
'200': { description: OK }
'404': { description: Not Found }
/api/players:
post:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Create player (Admin)
parameters: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
userId: { $ref: '#/components/schemas/Id' }
username: { type: string }
required: [userId,username]
responses:
'201': { description: Created }
x-roles: [Admin]
/api/players/{playerId}/experience:
post:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Adjust player experience (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
value: { type: integer }
required: [value]
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses:
'200': { description: OK }
x-roles: [Admin]
/api/players/{playerId}/mana:
post:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Adjust player mana (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
value: { type: integer }
required: [value]
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses:
'200': { description: OK }
x-roles: [Admin]
/api/players/top:
get:
tags: [Players]
security: [ { bearerAuth: [] } ]
summary: Top players by experience
parameters:
- in: query
name: count
schema: { type: integer, default: 10 }
responses:
'200': { description: OK }
/api/ranks:
get:
tags: [Ranks]
security: [ { bearerAuth: [] } ]
summary: List ranks
responses: { '200': { description: OK } }
post:
tags: [Ranks]
security: [ { bearerAuth: [] } ]
summary: Create rank (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
expNeeded: { type: integer }
required: [title,expNeeded]
responses: { '201': { description: Created } }
x-roles: [Admin]
/api/ranks/{id}:
get:
tags: [Ranks]
security: [ { bearerAuth: [] } ]
summary: Get rank
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
put:
tags: [Ranks]
security: [ { bearerAuth: [] } ]
summary: Update rank (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
expNeeded: { type: integer }
required: [title,expNeeded]
responses: { '200': { description: OK }, '404': { description: Not Found } }
x-roles: [Admin]
delete:
tags: [Ranks]
security: [ { bearerAuth: [] } ]
summary: Delete rank (Admin)
responses: { '204': { description: No Content }, '404': { description: Not Found } }
x-roles: [Admin]
/api/ranks/validate-advance/{playerId}/{targetRankId}:
get:
tags: [Ranks]
security: [ { bearerAuth: [] } ]
summary: Validate advancement
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
- in: path
name: targetRankId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/skills:
get:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: List skills
responses: { '200': { description: OK } }
post:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: Create skill (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
required: [title]
responses: { '201': { description: Created } }
x-roles: [Admin]
/api/skills/{id}:
get:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: Get skill
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
put:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: Update skill (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
required: [title]
responses: { '200': { description: OK }, '404': { description: Not Found } }
x-roles: [Admin]
delete:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: Delete skill (Admin)
responses: { '204': { description: No Content }, '404': { description: Not Found } }
x-roles: [Admin]
/api/skills/player/{playerId}:
get:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: List player skills
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/skills/player/{playerId}/{skillId}:
post:
tags: [Skills]
security: [ { bearerAuth: [] } ]
summary: Update player skill (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
level: { type: integer }
required: [level]
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
- in: path
name: skillId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
x-roles: [Admin]
/api/mission-categories:
get:
tags: [MissionCategories]
security: [ { bearerAuth: [] } ]
summary: List mission categories
responses: { '200': { description: OK } }
post:
tags: [MissionCategories]
security: [ { bearerAuth: [] } ]
summary: Create mission category (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
required: [title]
responses: { '201': { description: Created } }
x-roles: [Admin]
/api/mission-categories/{id}:
get:
tags: [MissionCategories]
security: [ { bearerAuth: [] } ]
summary: Get mission category
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
put:
tags: [MissionCategories]
security: [ { bearerAuth: [] } ]
summary: Update mission category (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
required: [title]
responses: { '200': { description: OK }, '404': { description: Not Found } }
x-roles: [Admin]
delete:
tags: [MissionCategories]
security: [ { bearerAuth: [] } ]
summary: Delete mission category (Admin)
responses: { '204': { description: No Content }, '404': { description: Not Found } }
x-roles: [Admin]
/api/missions/{id}:
get:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Get mission
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
put:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Update mission (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
description: { type: string }
missionCategoryId: { $ref: '#/components/schemas/Id' }
parentMissionId: { $ref: '#/components/schemas/Id', nullable: true }
expReward: { type: integer }
manaReward: { type: integer }
required: [title,missionCategoryId,expReward,manaReward]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
x-roles: [Admin]
delete:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Delete mission (Admin)
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '204': { description: No Content }, '404': { description: Not Found } }
x-roles: [Admin]
/api/missions/category/{categoryId}:
get:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Missions by category
parameters:
- in: path
name: categoryId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/missions/player/{playerId}/available:
get:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Available missions for player
parameters:
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/missions/{missionId}/rank-rules:
get:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Rank rules for mission
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/missions:
post:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Create mission (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
description: { type: string }
missionCategoryId: { $ref: '#/components/schemas/Id' }
parentMissionId: { $ref: '#/components/schemas/Id', nullable: true }
expReward: { type: integer }
manaReward: { type: integer }
required: [title,missionCategoryId,expReward,manaReward]
responses: { '201': { description: Created } }
x-roles: [Admin]
/api/missions/{id}:
put:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Update mission (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
title: { type: string }
description: { type: string }
missionCategoryId: { $ref: '#/components/schemas/Id' }
parentMissionId: { $ref: '#/components/schemas/Id', nullable: true }
expReward: { type: integer }
manaReward: { type: integer }
required: [title,missionCategoryId,expReward,manaReward]
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
x-roles: [Admin]
delete:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Delete mission (Admin)
parameters:
- in: path
name: id
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '204': { description: No Content }, '404': { description: Not Found } }
x-roles: [Admin]
/api/missions/{missionId}/complete:
post:
tags: [Missions]
security: [ { bearerAuth: [] } ]
summary: Complete mission
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
requestBody:
content:
application/json:
schema:
type: object
properties:
playerId: { $ref: '#/components/schemas/Id' }
proof: { nullable: true }
required: [playerId]
responses: { '200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/MissionCompletionResult' } } } }, '400': { description: Bad Request } }
/api/rewards/mission/{missionId}/skills:
get:
tags: [Rewards]
security: [ { bearerAuth: [] } ]
summary: Mission skill rewards
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/rewards/mission/{missionId}/items:
get:
tags: [Rewards]
security: [ { bearerAuth: [] } ]
summary: Mission item rewards
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/rewards/mission/{missionId}/can-claim/{playerId}:
get:
tags: [Rewards]
security: [ { bearerAuth: [] } ]
summary: Can claim mission rewards
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
- in: path
name: playerId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/rewards/mission/{missionId}/claim:
post:
tags: [Rewards]
security: [ { bearerAuth: [] } ]
summary: Claim mission rewards
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
requestBody:
content:
application/json:
schema:
type: object
properties:
playerId: { $ref: '#/components/schemas/Id' }
required: [playerId]
responses: { '200': { description: OK }, '409': { description: Conflict } }
/api/rewards/mission/{missionId}/force-distribute:
post:
tags: [Rewards]
security: [ { bearerAuth: [] } ]
summary: Force distribute mission rewards (Admin)
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
requestBody:
content:
application/json:
schema:
type: object
properties:
playerId: { $ref: '#/components/schemas/Id' }
required: [playerId]
responses: { '200': { description: OK } }
x-roles: [Admin]
/api/dialogue/mission/{missionId}:
get:
tags: [Dialogue]
security: [ { bearerAuth: [] } ]
summary: Get dialogue by mission
parameters:
- in: path
name: missionId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
/api/dialogue/message/{messageId}:
get:
tags: [Dialogue]
security: [ { bearerAuth: [] } ]
summary: Get dialogue message
parameters:
- in: path
name: messageId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK }, '404': { description: Not Found } }
/api/dialogue/message/{messageId}/options:
get:
tags: [Dialogue]
security: [ { bearerAuth: [] } ]
summary: Get dialogue response options
parameters:
- in: path
name: messageId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses: { '200': { description: OK } }
/api/dialogue/message/{messageId}/respond:
post:
tags: [Dialogue]
security: [ { bearerAuth: [] } ]
summary: Respond to dialogue
parameters:
- in: path
name: messageId
required: true
schema: { $ref: '#/components/schemas/Id' }
requestBody:
content:
application/json:
schema:
type: object
properties:
responseOptionId: { $ref: '#/components/schemas/Id' }
playerId: { $ref: '#/components/schemas/Id' }
required: [responseOptionId,playerId]
responses: { '200': { description: OK } }
/api/dialogue:
post:
tags: [Dialogue]
security: [ { bearerAuth: [] } ]
summary: Create dialogue (Admin)
requestBody:
content:
application/json:
schema:
type: object
properties:
missionId: { $ref: '#/components/schemas/Id' }
initialDialogueMessageId: { $ref: '#/components/schemas/Id' }
interimDialogueMessageId: { $ref: '#/components/schemas/Id' }
endDialogueMessageId: { $ref: '#/components/schemas/Id' }
required: [missionId,initialDialogueMessageId,interimDialogueMessageId,endDialogueMessageId]
responses: { '201': { description: Created } }
x-roles: [Admin]
/api/profile/me:
get:
tags: [Profile]
security: [ { bearerAuth: [] } ]
summary: Get current user profile
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } }
'404': { description: Not Found }
/api/profile/{userId}:
get:
tags: [Profile]
security: [ { bearerAuth: [] } ]
summary: Get profile by user id (Admin)
parameters:
- in: path
name: userId
required: true
schema: { $ref: '#/components/schemas/Id' }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } }
'404': { description: Not Found }
x-roles: [Admin]
/api/profile:
put:
tags: [Profile]
security: [ { bearerAuth: [] } ]
summary: Upsert current user profile
requestBody:
content:
application/json:
schema:
type: object
properties:
firstName: { type: string, nullable: true }
lastName: { type: string, nullable: true }
birthDate: { type: string, format: date, nullable: true }
about: { type: string, nullable: true }
location: { type: string, nullable: true }
responses:
'200': { description: OK, content: { application/json: { schema: { $ref: '#/components/schemas/Profile' } } } }
/api/profile/avatar:
post:
tags: [Profile]
security: [ { bearerAuth: [] } ]
summary: Upload avatar image (multipart/form-data)
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
responses:
'200': { description: OK, content: { application/json: { schema: { type: object, properties: { avatarUrl: { type: string } } } } } }
'400': { description: Bad Request }
delete:
tags: [Profile]
security: [ { bearerAuth: [] } ]
summary: Delete avatar image
responses:
'204': { description: No Content }
'404': { description: Not Found }