Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions database/sql/001_create_tables.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
-- ============================================================================
-- FantasyRealm Character Manager - Database Schema
-- PostgreSQL 15+
-- ============================================================================

-- ============================================================================
-- TABLE: role
-- User roles for authorization (user, employee, admin)
-- ============================================================================
CREATE TABLE role (
id SERIAL PRIMARY KEY,
label VARCHAR(50) NOT NULL UNIQUE
);

-- ============================================================================
-- TABLE: user
-- Registered users of the application
-- ============================================================================
CREATE TABLE "user" (
id SERIAL PRIMARY KEY,
pseudo VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
is_suspended BOOLEAN NOT NULL DEFAULT FALSE,
must_change_password BOOLEAN NOT NULL DEFAULT FALSE,
role_id INTEGER NOT NULL REFERENCES role(id)
);

CREATE INDEX idx_user_email ON "user"(email);
CREATE INDEX idx_user_pseudo ON "user"(pseudo);

-- ============================================================================
-- TABLE: character
-- Player characters created by users
-- ============================================================================
CREATE TABLE character (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
gender VARCHAR(20) NOT NULL CHECK (gender IN ('male', 'female')),
skin_color VARCHAR(7) NOT NULL,
eye_color VARCHAR(7) NOT NULL,
hair_color VARCHAR(7) NOT NULL,
eye_shape VARCHAR(50) NOT NULL,
nose_shape VARCHAR(50) NOT NULL,
mouth_shape VARCHAR(50) NOT NULL,
image TEXT,
is_shared BOOLEAN NOT NULL DEFAULT FALSE,
is_authorized BOOLEAN NOT NULL DEFAULT FALSE,
user_id INTEGER NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,

CONSTRAINT uq_character_name_per_user UNIQUE (name, user_id)
);

CREATE INDEX idx_character_user_id ON character(user_id);

-- ============================================================================
-- TABLE: article
-- Customization items (clothing, armor, weapons, accessories)
-- ============================================================================
CREATE TABLE article (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('clothing', 'armor', 'weapon', 'accessory')),
image TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);

CREATE INDEX idx_article_type ON article(type);
CREATE INDEX idx_article_is_active ON article(is_active);

-- ============================================================================
-- TABLE: character_article
-- Many-to-many relationship between characters and equipped articles
-- ============================================================================
CREATE TABLE character_article (
character_id INTEGER NOT NULL REFERENCES character(id) ON DELETE CASCADE,
article_id INTEGER NOT NULL REFERENCES article(id) ON DELETE CASCADE,

PRIMARY KEY (character_id, article_id)
);

-- ============================================================================
-- TABLE: comment
-- User reviews on shared characters
-- ============================================================================
CREATE TABLE comment (
id SERIAL PRIMARY KEY,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
text TEXT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved')),
commented_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
character_id INTEGER NOT NULL REFERENCES character(id) ON DELETE CASCADE,
author_id INTEGER NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,

CONSTRAINT uq_one_comment_per_user_per_character UNIQUE (character_id, author_id)
);

CREATE INDEX idx_comment_character_id ON comment(character_id);
CREATE INDEX idx_comment_author_id ON comment(author_id);
CREATE INDEX idx_comment_status ON comment(status);
117 changes: 117 additions & 0 deletions database/sql/002_seed_data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
-- ============================================================================
-- FantasyRealm Character Manager - Seed Data
-- PostgreSQL 15+
-- ============================================================================

-- ============================================================================
-- ROLES
-- ============================================================================
INSERT INTO role (label) VALUES
('user'),
('employee'),
('admin');

-- ============================================================================
-- ADMIN USER
-- Password: Admin123! (BCrypt hash)
-- ============================================================================
INSERT INTO "user" (pseudo, email, password_hash, role_id) VALUES
('admin', 'admin@fantasyrealm.com', '$2a$11$rBVcQ4z4.vYZ3mQlJXqKaeKxNJqX5vYxJqKqYQ5ZJ8z5X5X5X5X5q', (SELECT id FROM role WHERE label = 'admin'));

-- ============================================================================
-- EMPLOYEE USER (for testing moderation)
-- Password: Employee123!
-- ============================================================================
INSERT INTO "user" (pseudo, email, password_hash, role_id) VALUES
('moderator', 'moderator@fantasyrealm.com', '$2a$11$rBVcQ4z4.vYZ3mQlJXqKaeKxNJqX5vYxJqKqYQ5ZJ8z5X5X5X5X5q', (SELECT id FROM role WHERE label = 'employee'));

-- ============================================================================
-- TEST USERS
-- Password: Test123!
-- ============================================================================
INSERT INTO "user" (pseudo, email, password_hash, role_id) VALUES
('player_one', 'player1@test.com', '$2a$11$rBVcQ4z4.vYZ3mQlJXqKaeKxNJqX5vYxJqKqYQ5ZJ8z5X5X5X5X5q', (SELECT id FROM role WHERE label = 'user')),
('dragon_slayer', 'player2@test.com', '$2a$11$rBVcQ4z4.vYZ3mQlJXqKaeKxNJqX5vYxJqKqYQ5ZJ8z5X5X5X5X5q', (SELECT id FROM role WHERE label = 'user')),
('mystic_mage', 'player3@test.com', '$2a$11$rBVcQ4z4.vYZ3mQlJXqKaeKxNJqX5vYxJqKqYQ5ZJ8z5X5X5X5X5q', (SELECT id FROM role WHERE label = 'user'));

-- ============================================================================
-- ARTICLES - Clothing
-- ============================================================================
INSERT INTO article (name, type, image) VALUES
('Apprentice Robe', 'clothing', NULL),
('Leather Tunic', 'clothing', NULL),
('Noble Vest', 'clothing', NULL),
('Traveler Cloak', 'clothing', NULL),
('Silk Dress', 'clothing', NULL);

-- ============================================================================
-- ARTICLES - Armor
-- ============================================================================
INSERT INTO article (name, type, image) VALUES
('Iron Chestplate', 'armor', NULL),
('Steel Helmet', 'armor', NULL),
('Dragon Scale Armor', 'armor', NULL),
('Mithril Chainmail', 'armor', NULL),
('Guardian Shield', 'armor', NULL);

-- ============================================================================
-- ARTICLES - Weapons
-- ============================================================================
INSERT INTO article (name, type, image) VALUES
('Iron Sword', 'weapon', NULL),
('Oak Staff', 'weapon', NULL),
('Elven Bow', 'weapon', NULL),
('Battle Axe', 'weapon', NULL),
('Enchanted Dagger', 'weapon', NULL);

-- ============================================================================
-- ARTICLES - Accessories
-- ============================================================================
INSERT INTO article (name, type, image) VALUES
('Ruby Amulet', 'accessory', NULL),
('Silver Ring', 'accessory', NULL),
('Leather Belt', 'accessory', NULL),
('Explorer Backpack', 'accessory', NULL),
('Golden Crown', 'accessory', NULL);

-- ============================================================================
-- TEST CHARACTERS (authorized and shared for gallery demo)
-- ============================================================================
INSERT INTO character (name, gender, skin_color, eye_color, hair_color, eye_shape, nose_shape, mouth_shape, is_shared, is_authorized, user_id) VALUES
('Thorin', 'male', '#E8BEAC', '#4A90D9', '#2C1810', 'almond', 'aquiline', 'thin', TRUE, TRUE, (SELECT id FROM "user" WHERE pseudo = 'player_one')),
('Elara', 'female', '#F5DEB3', '#50C878', '#FFD700', 'round', 'straight', 'full', TRUE, TRUE, (SELECT id FROM "user" WHERE pseudo = 'dragon_slayer')),
('Zephyr', 'male', '#8D5524', '#8B4513', '#000000', 'narrow', 'wide', 'medium', TRUE, TRUE, (SELECT id FROM "user" WHERE pseudo = 'mystic_mage'));

-- ============================================================================
-- TEST CHARACTER (pending moderation)
-- ============================================================================
INSERT INTO character (name, gender, skin_color, eye_color, hair_color, eye_shape, nose_shape, mouth_shape, is_shared, is_authorized, user_id) VALUES
('Shadow', 'male', '#3D2314', '#FF0000', '#1C1C1C', 'narrow', 'pointed', 'thin', FALSE, FALSE, (SELECT id FROM "user" WHERE pseudo = 'player_one'));

-- ============================================================================
-- EQUIP ARTICLES TO CHARACTERS
-- ============================================================================
INSERT INTO character_article (character_id, article_id) VALUES
((SELECT id FROM character WHERE name = 'Thorin'), (SELECT id FROM article WHERE name = 'Iron Chestplate')),
((SELECT id FROM character WHERE name = 'Thorin'), (SELECT id FROM article WHERE name = 'Iron Sword')),
((SELECT id FROM character WHERE name = 'Thorin'), (SELECT id FROM article WHERE name = 'Leather Belt')),
((SELECT id FROM character WHERE name = 'Elara'), (SELECT id FROM article WHERE name = 'Silk Dress')),
((SELECT id FROM character WHERE name = 'Elara'), (SELECT id FROM article WHERE name = 'Elven Bow')),
((SELECT id FROM character WHERE name = 'Elara'), (SELECT id FROM article WHERE name = 'Ruby Amulet')),
((SELECT id FROM character WHERE name = 'Zephyr'), (SELECT id FROM article WHERE name = 'Apprentice Robe')),
((SELECT id FROM character WHERE name = 'Zephyr'), (SELECT id FROM article WHERE name = 'Oak Staff')),
((SELECT id FROM character WHERE name = 'Zephyr'), (SELECT id FROM article WHERE name = 'Silver Ring'));

-- ============================================================================
-- TEST COMMENTS (approved)
-- ============================================================================
INSERT INTO comment (rating, text, status, character_id, author_id) VALUES
(5, 'Amazing warrior design! Love the armor combo.', 'approved', (SELECT id FROM character WHERE name = 'Thorin'), (SELECT id FROM "user" WHERE pseudo = 'dragon_slayer')),
(4, 'Beautiful elven character, very elegant!', 'approved', (SELECT id FROM character WHERE name = 'Elara'), (SELECT id FROM "user" WHERE pseudo = 'player_one')),
(5, 'The mage aesthetic is perfect!', 'approved', (SELECT id FROM character WHERE name = 'Zephyr'), (SELECT id FROM "user" WHERE pseudo = 'dragon_slayer'));

-- ============================================================================
-- TEST COMMENT (pending moderation)
-- ============================================================================
INSERT INTO comment (rating, text, status, character_id, author_id) VALUES
(3, 'Nice but could use more accessories.', 'pending', (SELECT id FROM character WHERE name = 'Thorin'), (SELECT id FROM "user" WHERE pseudo = 'mystic_mage'));
87 changes: 47 additions & 40 deletions src/backend/src/FantasyRealm.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,50 @@
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// CORS configuration
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});

// TODO: Add JWT Authentication (FRO-1)
// TODO: Add Application services (FRO-15+)
// TODO: Add Infrastructure services (FRO-17)

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
namespace FantasyRealm.Api
{
app.UseSwagger();
app.UseSwaggerUI(options =>
public static class Program
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "FantasyRealm API v1");
});
private static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// CORS configuration
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:5173")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});

// TODO: Add JWT Authentication (FRO-1)
// TODO: Add Application services (FRO-15+)
// TODO: Add Infrastructure services (FRO-17)

var app = builder.Build();

// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "FantasyRealm API v1");
});
}

app.UseHttpsRedirection();
app.UseCors("AllowFrontend");
app.UseAuthorization();
app.MapControllers();

app.Run();
}
}
}

app.UseHttpsRedirection();
app.UseCors("AllowFrontend");
app.UseAuthorization();
app.MapControllers();

app.Run();

public partial class Program { }
9 changes: 8 additions & 1 deletion src/backend/src/FantasyRealm.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"ConnectionStrings": {
"PostgreSQL": "",
"MongoDB": ""
},
"MongoDB": {
"DatabaseName": "fantasyrealm"
}
}
Empty file.
40 changes: 40 additions & 0 deletions src/backend/src/FantasyRealm.Domain/Entities/ActivityLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using FantasyRealm.Domain.Enums;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;

namespace FantasyRealm.Domain.Entities
{
/// <summary>
/// Represents an activity log entry stored in MongoDB.
/// </summary>
public class ActivityLog
{
[BsonId]
public ObjectId Id { get; set; }

[BsonElement("timestamp")]
public DateTime Timestamp { get; set; }

[BsonElement("user_id")]
public int UserId { get; set; }

[BsonElement("user_pseudo")]
public string UserPseudo { get; set; } = string.Empty;

[BsonElement("action")]
[BsonRepresentation(BsonType.String)]
public ActivityAction Action { get; set; }

[BsonElement("target_type")]
public string TargetType { get; set; } = string.Empty;

[BsonElement("target_id")]
public int TargetId { get; set; }

[BsonElement("details")]
public BsonDocument? Details { get; set; }

[BsonElement("ip_address")]
public string? IpAddress { get; set; }
}
}
22 changes: 22 additions & 0 deletions src/backend/src/FantasyRealm.Domain/Entities/Article.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FantasyRealm.Domain.Enums;

namespace FantasyRealm.Domain.Entities
{
/// <summary>
/// Represents a customization item (clothing, armor, weapon, accessory).
/// </summary>
public class Article
{
public int Id { get; set; }

public string Name { get; set; } = string.Empty;

public ArticleType Type { get; set; }

public string? Image { get; set; }

public bool IsActive { get; set; } = true;

public ICollection<CharacterArticle> CharacterArticles { get; set; } = [];
}
}
Loading