Commit
This commit is contained in:
702
old/ACCOUNT_PLAYER_SEPARATION_PLAN.md
Normal file
702
old/ACCOUNT_PLAYER_SEPARATION_PLAN.md
Normal file
@@ -0,0 +1,702 @@
|
||||
# Account & Player Separation - Major Refactor Plan
|
||||
|
||||
## Overview
|
||||
Separate authentication (accounts) from gameplay (characters/players) to support:
|
||||
- Multiple characters per account
|
||||
- Free tier: 1 character
|
||||
- Premium tier: Up to 10 characters
|
||||
- Character customization at creation
|
||||
- Email-based login (no username)
|
||||
|
||||
---
|
||||
|
||||
## 1. New Database Schema
|
||||
|
||||
### Accounts Table (Authentication)
|
||||
```sql
|
||||
CREATE TABLE accounts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255), -- NULL for Steam/OAuth
|
||||
steam_id VARCHAR(255) UNIQUE, -- Steam integration
|
||||
account_type VARCHAR(20) DEFAULT 'web', -- 'web', 'steam'
|
||||
premium_expires_at TIMESTAMP, -- NULL = lifetime premium
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
email_verification_token VARCHAR(255),
|
||||
password_reset_token VARCHAR(255),
|
||||
password_reset_expires TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP,
|
||||
CONSTRAINT check_account_type CHECK (account_type IN ('web', 'steam'))
|
||||
);
|
||||
|
||||
CREATE INDEX idx_accounts_email ON accounts(email);
|
||||
CREATE INDEX idx_accounts_steam_id ON accounts(steam_id);
|
||||
```
|
||||
|
||||
### Characters Table (Gameplay)
|
||||
```sql
|
||||
CREATE TABLE characters (
|
||||
id SERIAL PRIMARY KEY,
|
||||
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) UNIQUE NOT NULL, -- Character name (unique across all players)
|
||||
avatar_data TEXT, -- JSON for avatar customization
|
||||
|
||||
-- RPG Stats
|
||||
level INTEGER DEFAULT 1,
|
||||
xp INTEGER DEFAULT 0,
|
||||
hp INTEGER DEFAULT 100,
|
||||
max_hp INTEGER DEFAULT 100,
|
||||
stamina INTEGER DEFAULT 100,
|
||||
max_stamina INTEGER DEFAULT 100,
|
||||
|
||||
-- Base Attributes (start with 0, player allocates 20 points)
|
||||
strength INTEGER DEFAULT 0,
|
||||
agility INTEGER DEFAULT 0,
|
||||
endurance INTEGER DEFAULT 0,
|
||||
intellect INTEGER DEFAULT 0,
|
||||
unspent_points INTEGER DEFAULT 20, -- Initial stat points to allocate
|
||||
|
||||
-- Game State
|
||||
location_id VARCHAR(255) DEFAULT 'cabin',
|
||||
is_dead BOOLEAN DEFAULT FALSE,
|
||||
last_movement_time REAL DEFAULT 0,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT check_unspent_points CHECK (unspent_points >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_characters_account_id ON characters(account_id);
|
||||
CREATE INDEX idx_characters_name ON characters(name);
|
||||
CREATE INDEX idx_characters_location_id ON characters(location_id);
|
||||
```
|
||||
|
||||
### Character Limits
|
||||
```sql
|
||||
-- Enforce character limits via application logic:
|
||||
-- Free accounts: MAX 1 character
|
||||
-- Premium accounts: MAX 10 characters
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Avatar System
|
||||
|
||||
### Avatar Data Structure (JSON)
|
||||
```json
|
||||
{
|
||||
"preset": "warrior", // Optional preset
|
||||
"body": {
|
||||
"skin_tone": "#f5d6c6",
|
||||
"build": "athletic" // slim, athletic, heavy
|
||||
},
|
||||
"hair": {
|
||||
"style": "short",
|
||||
"color": "#3d2817"
|
||||
},
|
||||
"face": {
|
||||
"eyes": "blue",
|
||||
"facial_hair": "none"
|
||||
},
|
||||
"equipped_display": {
|
||||
"helmet": "iron_helmet", // Shows equipped items on avatar
|
||||
"armor": "leather_chest",
|
||||
"weapon": "iron_sword"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Avatar Options
|
||||
**Phase 1 (MVP):** Simple presets
|
||||
- 10 preset avatars (warrior, mage, rogue, etc.)
|
||||
- Color variations
|
||||
|
||||
**Phase 2 (Future):** Dynamic avatar
|
||||
- Shows equipped armor/weapons
|
||||
- Customizable features
|
||||
- Level-based cosmetic unlocks
|
||||
|
||||
---
|
||||
|
||||
## 3. Migration Script
|
||||
|
||||
### `migrate_account_player_separation.py`
|
||||
```python
|
||||
"""
|
||||
Major migration: Separate accounts from characters
|
||||
1. Create accounts table
|
||||
2. Create characters table
|
||||
3. Migrate existing players to new structure
|
||||
4. Update all foreign keys
|
||||
5. Drop old players table (after backup)
|
||||
"""
|
||||
|
||||
Steps:
|
||||
1. Backup current players table
|
||||
2. Create accounts table
|
||||
3. For each existing player:
|
||||
- Create account with email (generate if missing)
|
||||
- Create character from player data
|
||||
- Migrate inventory, equipment, stats, etc.
|
||||
4. Update all foreign key references:
|
||||
- inventory.player_id -> character_id
|
||||
- equipment.player_id -> character_id
|
||||
- dropped_items references
|
||||
- combat references
|
||||
- etc.
|
||||
5. Test thoroughly
|
||||
6. Drop old players table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Authentication Flow Changes
|
||||
|
||||
### Registration (Email-based)
|
||||
```
|
||||
POST /api/auth/register
|
||||
{
|
||||
"email": "player@example.com",
|
||||
"password": "securepass"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"access_token": "...",
|
||||
"account": {
|
||||
"id": 1,
|
||||
"email": "player@example.com",
|
||||
"account_type": "web",
|
||||
"is_premium": false,
|
||||
"characters": [] // Empty on first register
|
||||
},
|
||||
"needs_character_creation": true
|
||||
}
|
||||
```
|
||||
|
||||
### Login (Email-based)
|
||||
```
|
||||
POST /api/auth/login
|
||||
{
|
||||
"email": "player@example.com",
|
||||
"password": "securepass"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"access_token": "...",
|
||||
"account": {...},
|
||||
"characters": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Aragorn",
|
||||
"level": 15,
|
||||
"avatar_data": {...},
|
||||
"last_played_at": "2025-11-09T..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Character Selection
|
||||
```
|
||||
POST /api/character/select
|
||||
{
|
||||
"character_id": 1
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"character": {...full character data...},
|
||||
"location": {...},
|
||||
"inventory": [...],
|
||||
"equipment": {...}
|
||||
}
|
||||
```
|
||||
|
||||
### Character Creation
|
||||
```
|
||||
POST /api/character/create
|
||||
{
|
||||
"name": "Aragorn",
|
||||
"avatar": {
|
||||
"preset": "warrior",
|
||||
...
|
||||
},
|
||||
"stats": {
|
||||
"strength": 8,
|
||||
"agility": 5,
|
||||
"endurance": 4,
|
||||
"intellect": 3
|
||||
} // Must total 20 points
|
||||
}
|
||||
|
||||
Validation:
|
||||
- Free users: Check character count < 1
|
||||
- Premium users: Check character count < 10
|
||||
- Name must be unique
|
||||
- Stats must total exactly 20
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. JWT Token Structure
|
||||
|
||||
### Old (Current)
|
||||
```json
|
||||
{
|
||||
"player_id": 1,
|
||||
"exp": 1699564800
|
||||
}
|
||||
```
|
||||
|
||||
### New
|
||||
```json
|
||||
{
|
||||
"account_id": 1,
|
||||
"character_id": 5, // Set after character selection
|
||||
"account_type": "web",
|
||||
"is_premium": false,
|
||||
"exp": 1699564800
|
||||
}
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
1. Login → Get token with `account_id`, no `character_id`
|
||||
2. Select character → New token with `character_id`
|
||||
3. All game endpoints require `character_id` in token
|
||||
|
||||
---
|
||||
|
||||
## 6. UI Changes Required
|
||||
|
||||
### A. Login/Register Screen Redesign
|
||||
|
||||
**Current:** Simple form
|
||||
**New:** Modern authentication UI
|
||||
|
||||
```tsx
|
||||
<AuthScreen>
|
||||
<Tabs>
|
||||
<Tab label="Login">
|
||||
<EmailInput />
|
||||
<PasswordInput />
|
||||
<Button>Login</Button>
|
||||
<Link>Forgot Password?</Link>
|
||||
</Tab>
|
||||
<Tab label="Register">
|
||||
<EmailInput />
|
||||
<PasswordInput />
|
||||
<PasswordConfirmInput />
|
||||
<Checkbox>I agree to Terms</Checkbox>
|
||||
<Button>Create Account</Button>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<Divider />
|
||||
<SteamLoginButton /> // Future
|
||||
</AuthScreen>
|
||||
```
|
||||
|
||||
**Design:**
|
||||
- Dark fantasy theme
|
||||
- Animated background (subtle fire/ash effects)
|
||||
- Elden Ring / Dark Souls inspired
|
||||
- Responsive (mobile-first)
|
||||
|
||||
### B. Character Selection Screen
|
||||
|
||||
```tsx
|
||||
<CharacterSelection>
|
||||
<Header>
|
||||
<AccountInfo email={account.email} />
|
||||
<PremiumBadge if={isPremium} />
|
||||
</Header>
|
||||
|
||||
<CharacterGrid>
|
||||
{characters.map(char => (
|
||||
<CharacterCard
|
||||
key={char.id}
|
||||
name={char.name}
|
||||
level={char.level}
|
||||
avatar={char.avatar_data}
|
||||
lastPlayed={char.last_played_at}
|
||||
onClick={() => selectCharacter(char.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{canCreateMore && (
|
||||
<CreateCharacterCard
|
||||
onClick={() => setShowCreation(true)}
|
||||
/>
|
||||
)}
|
||||
</CharacterGrid>
|
||||
|
||||
{!isPremium && characters.length >= 1 && (
|
||||
<UpgradeBanner>
|
||||
Upgrade to Premium for 9 more character slots!
|
||||
</UpgradeBanner>
|
||||
)}
|
||||
</CharacterSelection>
|
||||
```
|
||||
|
||||
### C. Character Creation Screen
|
||||
|
||||
```tsx
|
||||
<CharacterCreation>
|
||||
<Step1_Name>
|
||||
<Input
|
||||
placeholder="Enter character name"
|
||||
validation={checkNameUnique}
|
||||
/>
|
||||
</Step1_Name>
|
||||
|
||||
<Step2_Avatar>
|
||||
<AvatarPreview avatar={selectedAvatar} />
|
||||
<AvatarPresets>
|
||||
{presets.map(preset => (
|
||||
<PresetCard
|
||||
key={preset.id}
|
||||
image={preset.thumbnail}
|
||||
label={preset.name}
|
||||
onClick={() => setAvatar(preset)}
|
||||
/>
|
||||
))}
|
||||
</AvatarPresets>
|
||||
</Step2_Avatar>
|
||||
|
||||
<Step3_Stats>
|
||||
<StatAllocator
|
||||
remaining={pointsRemaining}
|
||||
stats={stats}
|
||||
onAllocate={(stat, amount) => allocateStat(stat, amount)}
|
||||
/>
|
||||
<StatsPreview>
|
||||
<Stat name="Strength" value={stats.strength} />
|
||||
<Stat name="Agility" value={stats.agility} />
|
||||
<Stat name="Endurance" value={stats.endurance} />
|
||||
<Stat name="Intellect" value={stats.intellect} />
|
||||
</StatsPreview>
|
||||
<PointsRemaining>{pointsRemaining} / 20</PointsRemaining>
|
||||
</Step3_Stats>
|
||||
|
||||
<Actions>
|
||||
<Button onClick={handleBack}>Back</Button>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
disabled={!isValid}
|
||||
primary
|
||||
>
|
||||
Create Character
|
||||
</Button>
|
||||
</Actions>
|
||||
</CharacterCreation>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Steam Integration Specifics
|
||||
|
||||
### Do You Need Two Executables?
|
||||
|
||||
**Answer: NO, one executable with runtime detection**
|
||||
|
||||
```typescript
|
||||
// At app startup
|
||||
const config = {
|
||||
isSteam: checkSteamRuntime(), // Detect Steam overlay
|
||||
apiUrl: process.env.API_URL || 'https://api.game.com',
|
||||
steamAppId: process.env.STEAM_APP_ID
|
||||
};
|
||||
|
||||
if (config.isSteam) {
|
||||
// Initialize Steamworks
|
||||
await initSteamworks();
|
||||
|
||||
// Auto-login with Steam
|
||||
const steamTicket = await getSteamAuthTicket();
|
||||
const authResponse = await api.post('/api/auth/steam/login', {
|
||||
steam_ticket: steamTicket
|
||||
});
|
||||
|
||||
// Skip email/password login, go straight to character selection
|
||||
} else {
|
||||
// Show email/password login
|
||||
}
|
||||
```
|
||||
|
||||
**Build Configuration:**
|
||||
```json
|
||||
{
|
||||
"builds": {
|
||||
"web": {
|
||||
"platform": "web",
|
||||
"steamworks": false
|
||||
},
|
||||
"steam-windows": {
|
||||
"platform": "windows",
|
||||
"steamworks": true,
|
||||
"steam_app_id": "1000000"
|
||||
},
|
||||
"steam-linux": {
|
||||
"platform": "linux",
|
||||
"steamworks": true
|
||||
},
|
||||
"standalone-windows": {
|
||||
"platform": "windows",
|
||||
"steamworks": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Tauri Build Setup
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
echoes-desktop/
|
||||
├── src-tauri/
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs
|
||||
│ │ ├── steam.rs # Steamworks integration
|
||||
│ │ ├── auth.rs # Authentication logic
|
||||
│ │ └── storage.rs # Local storage/cache
|
||||
│ ├── icons/
|
||||
│ ├── Cargo.toml
|
||||
│ └── tauri.conf.json
|
||||
├── src/ # Frontend (React)
|
||||
│ ├── components/
|
||||
│ ├── screens/
|
||||
│ │ ├── Auth.tsx # Login/Register
|
||||
│ │ ├── CharacterSelect.tsx
|
||||
│ │ ├── CharacterCreate.tsx
|
||||
│ │ └── Game.tsx
|
||||
│ └── main.tsx
|
||||
├── assets/ # Bundled assets
|
||||
└── package.json
|
||||
```
|
||||
|
||||
### Installation Steps
|
||||
```bash
|
||||
# 1. Install Tauri CLI
|
||||
cargo install tauri-cli
|
||||
|
||||
# 2. Create Tauri project
|
||||
npm create tauri-app
|
||||
|
||||
# 3. Configure build
|
||||
```
|
||||
|
||||
### tauri.conf.json
|
||||
```json
|
||||
{
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../dist"
|
||||
},
|
||||
"package": {
|
||||
"productName": "Echoes of the Ashes",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"fs": {
|
||||
"scope": ["$APPDATA/echoes-of-ashes/*"]
|
||||
},
|
||||
"http": {
|
||||
"scope": ["https://api.echoesoftheash.com/*"]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["msi", "app", "deb"], // Windows, Mac, Linux
|
||||
"identifier": "com.echoesoftheash.game",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["assets/*"], // Bundle game assets
|
||||
"externalBin": ["bin/steamworks"], // Steam DLL
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
},
|
||||
"security": {
|
||||
"csp": "default-src 'self'; connect-src 'self' https://api.echoesoftheash.com"
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://releases.echoesoftheash.com/{{target}}/{{current_version}}"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "YOUR_PUBLIC_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
"tauri:build:steam": "STEAM_ENABLED=true tauri build",
|
||||
"tauri:build:standalone": "STEAM_ENABLED=false tauri build"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Steamworks Integration (Rust)
|
||||
```rust
|
||||
// src-tauri/src/steam.rs
|
||||
use steamworks::Client;
|
||||
|
||||
pub struct SteamManager {
|
||||
client: Option<Client>,
|
||||
}
|
||||
|
||||
impl SteamManager {
|
||||
pub fn new(app_id: u32) -> Result<Self, String> {
|
||||
match Client::init_app(app_id) {
|
||||
Ok((client, _single)) => {
|
||||
Ok(Self { client: Some(client) })
|
||||
}
|
||||
Err(e) => Err(format!("Failed to init Steam: {:?}", e))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_steam_id(&self) -> Option<u64> {
|
||||
self.client.as_ref().map(|c| {
|
||||
c.user().steam_id().raw()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_auth_session_ticket(&self) -> Option<Vec<u8>> {
|
||||
// Implementation
|
||||
None
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementation Phases
|
||||
|
||||
### Phase 1: Database Refactor (Week 1)
|
||||
- [ ] Create migration script
|
||||
- [ ] Test migration on dev database
|
||||
- [ ] Create accounts + characters tables
|
||||
- [ ] Migrate existing data
|
||||
- [ ] Update all FK references
|
||||
- [ ] Test thoroughly
|
||||
|
||||
### Phase 2: Auth System (Week 1-2)
|
||||
- [ ] Email-based login/register
|
||||
- [ ] JWT with account_id + character_id
|
||||
- [ ] Character selection endpoint
|
||||
- [ ] Character creation endpoint
|
||||
- [ ] Character limit enforcement
|
||||
|
||||
### Phase 3: UI Redesign (Week 2-3)
|
||||
- [ ] New login/register screen
|
||||
- [ ] Character selection screen
|
||||
- [ ] Character creation screen
|
||||
- [ ] Avatar system (presets)
|
||||
- [ ] Stat allocation UI
|
||||
|
||||
### Phase 4: Steam Integration (Week 3-4)
|
||||
- [ ] Set up Steamworks SDK
|
||||
- [ ] Steam authentication backend
|
||||
- [ ] Steam auto-login flow
|
||||
- [ ] Test on Steam
|
||||
|
||||
### Phase 5: Tauri Desktop (Week 4-5)
|
||||
- [ ] Set up Tauri project
|
||||
- [ ] Asset bundling
|
||||
- [ ] Build pipeline
|
||||
- [ ] Steam runtime detection
|
||||
- [ ] Auto-updater
|
||||
- [ ] Test builds (Win/Mac/Linux)
|
||||
|
||||
### Phase 6: Testing & Polish (Week 5-6)
|
||||
- [ ] End-to-end testing
|
||||
- [ ] Performance optimization
|
||||
- [ ] Bug fixes
|
||||
- [ ] Documentation
|
||||
- [ ] Beta release
|
||||
|
||||
---
|
||||
|
||||
## 10. Breaking Changes & Risks
|
||||
|
||||
### Database
|
||||
- **MAJOR:** Complete schema change
|
||||
- **Risk:** Data loss if migration fails
|
||||
- **Mitigation:** Full backup before migration, rollback plan
|
||||
|
||||
### Authentication
|
||||
- **MAJOR:** Login now uses email, not username
|
||||
- **Risk:** Existing users can't login
|
||||
- **Mitigation:** Send email to all users about change
|
||||
|
||||
### API
|
||||
- **MAJOR:** Most endpoints change from player_id to character_id
|
||||
- **Risk:** All API clients break
|
||||
- **Mitigation:** Version API (v2), deprecate v1
|
||||
|
||||
### Frontend
|
||||
- **MAJOR:** Complete auth flow redesign
|
||||
- **Risk:** UX confusion
|
||||
- **Mitigation:** Tutorial on first login after update
|
||||
|
||||
---
|
||||
|
||||
## 11. Rollback Plan
|
||||
|
||||
If migration fails:
|
||||
1. Restore database from backup
|
||||
2. Revert code changes
|
||||
3. Restart containers with old version
|
||||
4. Investigate issue
|
||||
5. Fix and retry
|
||||
|
||||
**Backup Strategy:**
|
||||
```bash
|
||||
# Before migration
|
||||
docker exec echoes_of_the_ashes_db pg_dump -U postgres gamedb > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Restore if needed
|
||||
docker exec -i echoes_of_the_ashes_db psql -U postgres gamedb < backup_20251109.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Review this plan** - Confirm approach
|
||||
2. **Create detailed migration script** - Handle all edge cases
|
||||
3. **Set up dev environment** - Test migration there first
|
||||
4. **Implement Phase 1** - Database refactor
|
||||
5. **Update authentication** - Email-based login
|
||||
6. **Build UI screens** - Character selection/creation
|
||||
7. **Integrate Steam** - Steamworks SDK
|
||||
8. **Create Tauri build** - Desktop client
|
||||
|
||||
**Estimated Timeline:** 6 weeks full-time
|
||||
|
||||
**Do you want me to start implementing Phase 1 (database refactor)?**
|
||||
Reference in New Issue
Block a user