diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cc1396..5a51ca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,43 @@ # π ΠΡΡΠΎΡΠΈΡ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠΉ MC Panel +## ΠΠ΅ΡΡΠΈΡ 2.1 - ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ (14.01.2026) + +### β¨ ΠΠΎΠ²ΡΠ΅ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ + +#### π€ ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ +- ΠΠ½ΠΎΠΏΠΊΠ° "ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ" Π² header ΡΡΠ΄ΠΎΠΌ Ρ "Π’ΠΈΠΊΠ΅ΡΡ" +- Π’ΡΠΈ Π²ΠΊΠ»Π°Π΄ΠΊΠΈ: ΠΠ±Π·ΠΎΡ, ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ, ΠΠ°ΡΠΎΠ»Ρ +- Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΡΠΎΡΠΈΠ»Ρ (ΡΠ΅ΡΠ²Π΅ΡΡ, ΡΠΈΠΊΠ΅ΡΡ, ΡΠΎΠ»Ρ) +- Π‘ΠΏΠΈΡΠΎΠΊ ΡΠ²ΠΎΠΈΡ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² +- ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΈΠΌΠ΅Π½ΠΈ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Ρ ΠΏΠΎΠ΄ΡΠ²Π΅ΡΠΆΠ΄Π΅Π½ΠΈΠ΅ΠΌ ΠΏΠ°ΡΠΎΠ»Π΅ΠΌ +- ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Ρ Ρ ΠΏΡΠΎΠ²Π΅ΡΠΊΠΎΠΉ +- ΠΠΎΠΊΠ°Π·/ΡΠΊΡΡΡΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Π΅ΠΉ Π² ΡΠΎΡΠΌΠ°Ρ + +#### π ΠΠ΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ +- ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΠΎΡΡΠΈ ΠΈΠΌΠ΅Π½ΠΈ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +- ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π²Π»Π°Π΄Π΅Π»ΡΡΠ΅Π² ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² ΠΏΡΠΈ ΡΠΌΠ΅Π½Π΅ ΠΈΠΌΠ΅Π½ΠΈ +- ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π΄ΠΎΡΡΡΠΏΠΎΠ² ΠΊ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌ +- ΠΠΎΠ²ΡΠΉ JWT ΡΠΎΠΊΠ΅Π½ ΠΏΡΠΈ ΡΠΌΠ΅Π½Π΅ ΠΈΠΌΠ΅Π½ΠΈ +- Π₯Π΅ΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Π΅ΠΉ (bcrypt) + +#### π Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΡΠΎΡΠΈΠ»Ρ +- ΠΠ±ΡΠ΅Π΅ ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² +- ΠΠΎΠΈ ΡΠ΅ΡΠ²Π΅ΡΡ (Π²Π»Π°Π΄Π΅Π»Π΅Ρ) +- ΠΠΎΡΡΡΠΏΠ½ΡΠ΅ ΡΠ΅ΡΠ²Π΅ΡΡ +- Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΏΠΎ ΡΠΈΠΊΠ΅ΡΠ°ΠΌ (Π²ΡΠ΅Π³ΠΎ, Π½Π° ΡΠ°ΡΡΠΌΠΎΡΡΠ΅Π½ΠΈΠΈ, Π² ΡΠ°Π±ΠΎΡΠ΅, Π·Π°ΠΊΡΡΡΠΎ) +- ΠΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ ΠΎ ΡΠΎΠ»ΠΈ + +### π ΠΠΎΠ²ΡΠ΅ ΡΠ°ΠΉΠ»Ρ +- `frontend/src/components/Profile.jsx` - ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° +- `PROFILE_SYSTEM.md` - Π΄ΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΡ Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° + +### π§ API Endpoints +- `PUT /api/profile/username` - ΠΈΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +- `PUT /api/profile/password` - ΠΈΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΏΠ°ΡΠΎΠ»Ρ +- `GET /api/profile/stats` - ΠΏΠΎΠ»ΡΡΠΈΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ ΠΏΡΠΎΡΠΈΠ»Ρ + +--- + ## ΠΠ΅ΡΡΠΈΡ 2.0 - Π‘ΠΈΡΡΠ΅ΠΌΠ° ΡΠΈΠΊΠ΅ΡΠΎΠ² (14.01.2026) ### β¨ ΠΠΎΠ²ΡΠ΅ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ diff --git a/PROFILE_SYSTEM.md b/PROFILE_SYSTEM.md new file mode 100644 index 0000000..148cf10 --- /dev/null +++ b/PROFILE_SYSTEM.md @@ -0,0 +1,228 @@ +# π€ Π‘ΠΈΡΡΠ΅ΠΌΠ° Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° + +## Π§ΡΠΎ Π΄ΠΎΠ±Π°Π²Π»Π΅Π½ΠΎ + +### β ΠΠΎΠ²ΡΠ΅ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ + +1. **ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ** - ΠΏΠΎΠ»Π½ΠΎΡΠ΅Π½Π½Π°Ρ ΡΠΈΡΡΠ΅ΠΌΠ° ΡΠΏΡΠ°Π²Π»Π΅Π½ΠΈΡ ΠΏΡΠΎΡΠΈΠ»Π΅ΠΌ +2. **Π’ΡΠΈ Π²ΠΊΠ»Π°Π΄ΠΊΠΈ**: + - π **ΠΠ±Π·ΠΎΡ** - ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠ° ΠΈ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ ΠΎ ΠΏΡΠΎΡΠΈΠ»Π΅ + - π€ **ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ** - ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΈΠΌΠ΅Π½ΠΈ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ + - π **ΠΠ°ΡΠΎΠ»Ρ** - ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Ρ + +3. **ΠΠ½ΠΎΠΏΠΊΠ° "ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ"** Π² header ΡΡΠ΄ΠΎΠΌ Ρ ΠΊΠ½ΠΎΠΏΠΊΠΎΠΉ "Π’ΠΈΠΊΠ΅ΡΡ" + +### π ΠΠΊΠ»Π°Π΄ΠΊΠ° "ΠΠ±Π·ΠΎΡ" + +#### ΠΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ ΠΎ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅ +- ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +- Π ΠΎΠ»Ρ (ΠΠ΄ΠΌΠΈΠ½ΠΈΡΡΡΠ°ΡΠΎΡ, Π’Π΅Ρ . ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠΊΠ°, ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ) +- Π¦Π²Π΅ΡΠ½ΠΎΠΉ Π±Π΅ΠΉΠ΄ΠΆ ΡΠΎΠ»ΠΈ + +#### Π‘ΡΠ°ΡΠΈΡΡΠΈΠΊΠ° +- **Π‘Π΅ΡΠ²Π΅ΡΡ**: + - ΠΡΠ΅Π³ΠΎ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² + - ΠΠΎΠΈ ΡΠ΅ΡΠ²Π΅ΡΡ (Π²Π»Π°Π΄Π΅Π»Π΅Ρ) + - ΠΠΎΡΡΡΠΏΠ½ΡΠ΅ ΡΠ΅ΡΠ²Π΅ΡΡ + +- **Π’ΠΈΠΊΠ΅ΡΡ**: + - ΠΡΠ΅Π³ΠΎ ΡΠΈΠΊΠ΅ΡΠΎΠ² + - ΠΠ° ΡΠ°ΡΡΠΌΠΎΡΡΠ΅Π½ΠΈΠΈ + - Π ΡΠ°Π±ΠΎΡΠ΅ + - ΠΠ°ΠΊΡΡΡΠΎ + +- **Π ΠΎΠ»Ρ**: + - ΠΠ°Π·Π²Π°Π½ΠΈΠ΅ ΡΠΎΠ»ΠΈ + - ΠΠΏΠΈΡΠ°Π½ΠΈΠ΅ ΠΏΡΠ°Π² + +#### Π‘ΠΏΠΈΡΠΎΠΊ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² +- ΠΡΠΎΠ±ΡΠ°ΠΆΠ΅Π½ΠΈΠ΅ Π²ΡΠ΅Ρ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +- ΠΠ°Π·Π²Π°Π½ΠΈΠ΅ ΠΈ ID ΡΠ΅ΡΠ²Π΅ΡΠ° +- ΠΡΠ°ΡΠΈΠ²ΡΠ΅ ΠΊΠ°ΡΡΠΎΡΠΊΠΈ + +### π€ ΠΠΊΠ»Π°Π΄ΠΊΠ° "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ" + +#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ +- ΠΡΠΎΡΠΌΠΎΡΡ ΡΠ΅ΠΊΡΡΠ΅Π³ΠΎ ΠΈΠΌΠ΅Π½ΠΈ +- ΠΠ²ΠΎΠ΄ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΈΠΌΠ΅Π½ΠΈ (ΠΌΠΈΠ½ΠΈΠΌΡΠΌ 3 ΡΠΈΠΌΠ²ΠΎΠ»Π°) +- ΠΠΎΠ΄ΡΠ²Π΅ΡΠΆΠ΄Π΅Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Π΅ΠΌ +- ΠΠ²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈΠΉ ΠΏΠ΅ΡΠ΅Π»ΠΎΠ³ΠΈΠ½ Ρ Π½ΠΎΠ²ΡΠΌ ΠΈΠΌΠ΅Π½Π΅ΠΌ + +#### ΠΠ΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ +- ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΠΎΡΡΠΈ ΠΈΠΌΠ΅Π½ΠΈ +- ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΠΏΠ°ΡΠΎΠ»Ρ +- ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π²Π»Π°Π΄Π΅Π»ΡΡΠ΅Π² ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² +- ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π΄ΠΎΡΡΡΠΏΠΎΠ² ΠΊ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌ +- ΠΠΎΠ²ΡΠΉ JWT ΡΠΎΠΊΠ΅Π½ + +### π ΠΠΊΠ»Π°Π΄ΠΊΠ° "ΠΠ°ΡΠΎΠ»Ρ" + +#### ΠΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡΠΈ +- ΠΠ²ΠΎΠ΄ ΡΠ΅ΠΊΡΡΠ΅Π³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ +- ΠΠ²ΠΎΠ΄ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ (ΠΌΠΈΠ½ΠΈΠΌΡΠΌ 6 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²) +- ΠΠΎΠ΄ΡΠ²Π΅ΡΠΆΠ΄Π΅Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ +- ΠΠΎΠΊΠ°Π·/ΡΠΊΡΡΡΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Π΅ΠΉ + +#### ΠΠ΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ +- ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΡΠ΅ΠΊΡΡΠ΅Π³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ +- ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΡΠΎΠ²ΠΏΠ°Π΄Π΅Π½ΠΈΡ Π½ΠΎΠ²ΡΡ ΠΏΠ°ΡΠΎΠ»Π΅ΠΉ +- Π₯Π΅ΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Ρ (bcrypt) + +## π ΠΠ°ΠΊ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ + +### ΠΡΠΊΡΡΡΠΈΠ΅ Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° +1. ΠΠ°ΠΆΠΌΠΈΡΠ΅ ΠΊΠ½ΠΎΠΏΠΊΡ "ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ" Π² header +2. ΠΡΠΊΡΠΎΠ΅ΡΡΡ ΡΡΡΠ°Π½ΠΈΡΠ° Ρ ΡΡΠ΅ΠΌΡ Π²ΠΊΠ»Π°Π΄ΠΊΠ°ΠΌΠΈ + +### ΠΡΠΎΡΠΌΠΎΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠΈ +1. ΠΡΠΊΡΠΎΠΉΡΠ΅ Π²ΠΊΠ»Π°Π΄ΠΊΡ "ΠΠ±Π·ΠΎΡ" +2. ΠΠΎΡΠΌΠΎΡΡΠΈΡΠ΅ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ ΠΎ ΠΏΡΠΎΡΠΈΠ»Π΅ +3. ΠΠΎΡΠΌΠΎΡΡΠΈΡΠ΅ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ ΠΏΠΎ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌ ΠΈ ΡΠΈΠΊΠ΅ΡΠ°ΠΌ +4. ΠΠΎΡΠΌΠΎΡΡΠΈΡΠ΅ ΡΠΏΠΈΡΠΎΠΊ ΡΠ²ΠΎΠΈΡ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² + +### ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΈΠΌΠ΅Π½ΠΈ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +1. ΠΡΠΊΡΠΎΠΉΡΠ΅ Π²ΠΊΠ»Π°Π΄ΠΊΡ "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ" +2. ΠΠ²Π΅Π΄ΠΈΡΠ΅ Π½ΠΎΠ²ΠΎΠ΅ ΠΈΠΌΡ (ΠΌΠΈΠ½ΠΈΠΌΡΠΌ 3 ΡΠΈΠΌΠ²ΠΎΠ»Π°) +3. ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΡΠ΅ΠΊΡΡΠΈΠΉ ΠΏΠ°ΡΠΎΠ»Ρ Π΄Π»Ρ ΠΏΠΎΠ΄ΡΠ²Π΅ΡΠΆΠ΄Π΅Π½ΠΈΡ +4. ΠΠ°ΠΆΠΌΠΈΡΠ΅ "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ" +5. ΠΡ Π±ΡΠ΄Π΅ΡΠ΅ Π°Π²ΡΠΎΠΌΠ°ΡΠΈΡΠ΅ΡΠΊΠΈ ΠΏΠ΅ΡΠ΅Π»ΠΎΠ³ΠΈΠ½Π΅Π½Ρ + +β οΈ **ΠΠ°ΠΆΠ½ΠΎ**: ΠΠΎΡΠ»Π΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΠΈΠΌΠ΅Π½ΠΈ: +- ΠΠ±Π½ΠΎΠ²ΡΡΡΡ Π²ΡΠ΅ ΡΠ΅ΡΠ²Π΅ΡΡ, Π³Π΄Π΅ Π²Ρ Π²Π»Π°Π΄Π΅Π»Π΅Ρ +- ΠΠ±Π½ΠΎΠ²ΡΡΡΡ Π²ΡΠ΅ Π΄ΠΎΡΡΡΠΏΡ ΠΊ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌ +- ΠΡ ΠΏΠΎΠ»ΡΡΠΈΡΠ΅ Π½ΠΎΠ²ΡΠΉ ΡΠΎΠΊΠ΅Π½ Π°Π²ΡΠΎΡΠΈΠ·Π°ΡΠΈΠΈ + +### ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Ρ +1. ΠΡΠΊΡΠΎΠΉΡΠ΅ Π²ΠΊΠ»Π°Π΄ΠΊΡ "ΠΠ°ΡΠΎΠ»Ρ" +2. ΠΠ²Π΅Π΄ΠΈΡΠ΅ ΡΠ΅ΠΊΡΡΠΈΠΉ ΠΏΠ°ΡΠΎΠ»Ρ +3. ΠΠ²Π΅Π΄ΠΈΡΠ΅ Π½ΠΎΠ²ΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ (ΠΌΠΈΠ½ΠΈΠΌΡΠΌ 6 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²) +4. ΠΠΎΠ΄ΡΠ²Π΅ΡΠ΄ΠΈΡΠ΅ Π½ΠΎΠ²ΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ +5. ΠΠ°ΠΆΠΌΠΈΡΠ΅ "ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΏΠ°ΡΠΎΠ»Ρ" + +β οΈ **ΠΠ°ΠΆΠ½ΠΎ**: ΠΠΎΡΠ»Π΅ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΡ ΠΏΠ°ΡΠΎΠ»Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ Π½ΠΎΠ²ΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ Π΄Π»Ρ Π²Ρ ΠΎΠ΄Π°. + +## π ΠΠΎΠ²ΡΠ΅ ΡΠ°ΠΉΠ»Ρ + +### Backend +- ΠΠΎΠ±Π°Π²Π»Π΅Π½Ρ endpoints Π² `backend/main.py`: + - `PUT /api/profile/username` - ΠΈΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ + - `PUT /api/profile/password` - ΠΈΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΏΠ°ΡΠΎΠ»Ρ + - `GET /api/profile/stats` - ΠΏΠΎΠ»ΡΡΠΈΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ ΠΏΡΠΎΡΠΈΠ»Ρ + +### Frontend +- `frontend/src/components/Profile.jsx` - ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° + +## π¨ ΠΠ½ΡΠ΅ΡΡΠ΅ΠΉΡ + +### ΠΠΊΠ»Π°Π΄ΠΊΠΈ +- Π‘ΠΎΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΡΠΉ Π΄ΠΈΠ·Π°ΠΉΠ½ Ρ Π²ΠΊΠ»Π°Π΄ΠΊΠ°ΠΌΠΈ +- ΠΠ»Π°Π²Π½ΡΠ΅ ΠΏΠ΅ΡΠ΅Ρ ΠΎΠ΄Ρ ΠΌΠ΅ΠΆΠ΄Ρ Π²ΠΊΠ»Π°Π΄ΠΊΠ°ΠΌΠΈ +- ΠΠ΄Π°ΠΏΡΠΈΠ²Π½ΡΠΉ Π΄ΠΈΠ·Π°ΠΉΠ½ + +### ΠΠ°ΡΡΠΎΡΠΊΠΈ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΠΈ +- Π¦Π²Π΅ΡΠ½ΡΠ΅ ΠΈΠΊΠΎΠ½ΠΊΠΈ +- Π§ΠΈΡΠ»ΠΎΠ²ΡΠ΅ ΠΏΠΎΠΊΠ°Π·Π°ΡΠ΅Π»ΠΈ +- ΠΠ΅ΡΠ°Π»ΡΠ½Π°Ρ ΠΈΠ½ΡΠΎΡΠΌΠ°ΡΠΈΡ + +### Π€ΠΎΡΠΌΡ +- ΠΠ°Π»ΠΈΠ΄Π°ΡΠΈΡ ΠΏΠΎΠ»Π΅ΠΉ +- ΠΠΎΠΊΠ°Π·/ΡΠΊΡΡΡΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Π΅ΠΉ +- ΠΡΠ΅Π΄ΡΠΏΡΠ΅ΠΆΠ΄Π΅Π½ΠΈΡ ΠΎ ΠΏΠΎΡΠ»Π΅Π΄ΡΡΠ²ΠΈΡΡ +- ΠΠ½ΠΎΠΏΠΊΠΈ Ρ ΠΈΠ½Π΄ΠΈΠΊΠ°ΡΠΈΠ΅ΠΉ Π·Π°Π³ΡΡΠ·ΠΊΠΈ + +## π§ Π’Π΅Ρ Π½ΠΈΡΠ΅ΡΠΊΠΈΠ΅ Π΄Π΅ΡΠ°Π»ΠΈ + +### API Endpoints + +#### PUT /api/profile/username +ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ + +**Request:** +```json +{ + "new_username": "NewUsername", + "password": "current_password" +} +``` + +**Response:** +```json +{ + "message": "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΎ", + "access_token": "new_jwt_token", + "username": "NewUsername" +} +``` + +#### PUT /api/profile/password +ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΏΠ°ΡΠΎΠ»Ρ + +**Request:** +```json +{ + "old_password": "old_password", + "new_password": "new_password" +} +``` + +**Response:** +```json +{ + "message": "ΠΠ°ΡΠΎΠ»Ρ ΠΈΠ·ΠΌΠ΅Π½ΡΠ½" +} +``` + +#### GET /api/profile/stats +ΠΠΎΠ»ΡΡΠΈΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ ΠΏΡΠΎΡΠΈΠ»Ρ + +**Response:** +```json +{ + "username": "username", + "role": "user", + "owned_servers": [ + { + "name": "server1", + "displayName": "My Server" + } + ], + "accessible_servers": [], + "tickets": { + "total": 5, + "pending": 2, + "in_progress": 1, + "closed": 2 + }, + "total_servers": 1 +} +``` + +### ΠΠ΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡΡ + +#### ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΈΠΌΠ΅Π½ΠΈ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +1. ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΠΏΠ°ΡΠΎΠ»Ρ +2. ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΠΎΡΡΠΈ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΈΠΌΠ΅Π½ΠΈ +3. ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π²Π»Π°Π΄Π΅Π»ΡΡΠ΅Π² ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² +4. ΠΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π΄ΠΎΡΡΡΠΏΠΎΠ² ΠΊ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌ +5. Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠ³ΠΎ JWT ΡΠΎΠΊΠ΅Π½Π° + +#### ΠΠ·ΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ ΠΏΠ°ΡΠΎΠ»Ρ +1. ΠΡΠΎΠ²Π΅ΡΠΊΠ° ΡΠ΅ΠΊΡΡΠ΅Π³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ +2. ΠΡΠΎΠ²Π΅ΡΠΊΠ° Π΄Π»ΠΈΠ½Ρ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ (ΠΌΠΈΠ½ΠΈΠΌΡΠΌ 6 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²) +3. Π₯Π΅ΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΠ°ΡΠΎΠ»Ρ (bcrypt) + +## β ΠΠΎΡΠΎΠ²ΠΎ! + +Π‘ΠΈΡΡΠ΅ΠΌΠ° Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° ΠΏΠΎΠ»Π½ΠΎΡΡΡΡ ΠΈΠ½ΡΠ΅Π³ΡΠΈΡΠΎΠ²Π°Π½Π° Π² MC Panel. ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΠΈ ΠΌΠΎΠ³ΡΡ: +- ΠΡΠΎΡΠΌΠ°ΡΡΠΈΠ²Π°ΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ ΡΠ²ΠΎΠ΅Π³ΠΎ ΠΏΡΠΎΡΠΈΠ»Ρ +- ΠΠ·ΠΌΠ΅Π½ΡΡΡ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ +- ΠΠ·ΠΌΠ΅Π½ΡΡΡ ΠΏΠ°ΡΠΎΠ»Ρ +- ΠΠΈΠ΄Π΅ΡΡ ΡΠ²ΠΎΠΈ ΡΠ΅ΡΠ²Π΅ΡΡ ΠΈ ΡΠΈΠΊΠ΅ΡΡ + +### ΠΠΎΡΡΡΠΏ ΠΊ Π»ΠΈΡΠ½ΠΎΠΌΡ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΡ +ΠΠ½ΠΎΠΏΠΊΠ° "ΠΠΈΡΠ½ΡΠΉ ΠΊΠ°Π±ΠΈΠ½Π΅Ρ" Π΄ΠΎΡΡΡΠΏΠ½Π° Π²ΡΠ΅ΠΌ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»ΡΠΌ Π² header ΡΡΠ΄ΠΎΠΌ Ρ ΠΊΠ½ΠΎΠΏΠΊΠΎΠΉ "Π’ΠΈΠΊΠ΅ΡΡ". + +### Π£ΡΡΡΠ½ΡΠ΅ Π΄Π°Π½Π½ΡΠ΅ ΠΏΠΎ ΡΠΌΠΎΠ»ΡΠ°Π½ΠΈΡ +- **ΠΠΎΠ³ΠΈΠ½**: Sofa12345 +- **ΠΠ°ΡΠΎΠ»Ρ**: arkonsad123 +- **Π ΠΎΠ»Ρ**: admin diff --git a/backend/main.py b/backend/main.py index 02183c9..c112e96 100644 --- a/backend/main.py +++ b/backend/main.py @@ -266,7 +266,7 @@ async def update_user_role(username: str, data: dict, user: dict = Depends(get_c raise HTTPException(404, "ΠΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π½Π΅ Π½Π°ΠΉΠ΄Π΅Π½") new_role = data.get("role") - if new_role not in ["admin", "user"]: + if new_role not in ["admin", "user", "support"]: raise HTTPException(400, "ΠΠ΅Π²Π΅ΡΠ½Π°Ρ ΡΠΎΠ»Ρ") users[username]["role"] = new_role @@ -274,6 +274,130 @@ async def update_user_role(username: str, data: dict, user: dict = Depends(get_c return {"message": "Π ΠΎΠ»Ρ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½Π°"} +# API Π΄Π»Ρ Π»ΠΈΡΠ½ΠΎΠ³ΠΎ ΠΊΠ°Π±ΠΈΠ½Π΅ΡΠ° +@app.put("/api/profile/username") +async def update_username(data: dict, user: dict = Depends(get_current_user)): + """ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ""" + new_username = data.get("new_username", "").strip() + password = data.get("password", "") + + if not new_username: + raise HTTPException(400, "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π½Π΅ ΠΌΠΎΠΆΠ΅Ρ Π±ΡΡΡ ΠΏΡΡΡΡΠΌ") + + if len(new_username) < 3: + raise HTTPException(400, "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±ΡΡΡ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ 3 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²") + + users = load_users() + + # ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ ΠΏΠ°ΡΠΎΠ»Ρ + if not verify_password(password, users[user["username"]]["password"]): + raise HTTPException(400, "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ") + + # ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ, Π½Π΅ Π·Π°Π½ΡΡΠΎ Π»ΠΈ Π½ΠΎΠ²ΠΎΠ΅ ΠΈΠΌΡ + if new_username in users and new_username != user["username"]: + raise HTTPException(400, "ΠΡΠΎ ΠΈΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΡΠΆΠ΅ Π·Π°Π½ΡΡΠΎ") + + # Π‘ΠΎΡ ΡΠ°Π½ΡΠ΅ΠΌ Π΄Π°Π½Π½ΡΠ΅ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ + old_username = user["username"] + user_data = users[old_username] + + # Π£Π΄Π°Π»ΡΠ΅ΠΌ ΡΡΠ°ΡΡΡ Π·Π°ΠΏΠΈΡΡ ΠΈ ΡΠΎΠ·Π΄Π°ΡΠΌ Π½ΠΎΠ²ΡΡ + del users[old_username] + user_data["username"] = new_username + users[new_username] = user_data + + # ΠΠ±Π½ΠΎΠ²Π»ΡΠ΅ΠΌ Π²Π»Π°Π΄Π΅Π»ΡΡΠ΅Π² ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² + for server_dir in SERVERS_DIR.iterdir(): + if server_dir.is_dir(): + config = load_server_config(server_dir.name) + if config.get("owner") == old_username: + config["owner"] = new_username + save_server_config(server_dir.name, config) + + # ΠΠ±Π½ΠΎΠ²Π»ΡΠ΅ΠΌ Π΄ΠΎΡΡΡΠΏΡ ΠΊ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌ Ρ Π΄ΡΡΠ³ΠΈΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Π΅ΠΉ + for username, user_info in users.items(): + if "servers" in user_info and old_username in user_info.get("servers", []): + user_info["servers"] = [new_username if s == old_username else s for s in user_info["servers"]] + + save_users(users) + + # Π‘ΠΎΠ·Π΄Π°ΡΠΌ Π½ΠΎΠ²ΡΠΉ ΡΠΎΠΊΠ΅Π½ + new_token = create_access_token({"sub": new_username, "role": user_data["role"]}) + + return { + "message": "ΠΠΌΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ ΠΈΠ·ΠΌΠ΅Π½Π΅Π½ΠΎ", + "access_token": new_token, + "username": new_username + } + +@app.put("/api/profile/password") +async def update_password(data: dict, user: dict = Depends(get_current_user)): + """ΠΠ·ΠΌΠ΅Π½ΠΈΡΡ ΠΏΠ°ΡΠΎΠ»Ρ""" + old_password = data.get("old_password", "") + new_password = data.get("new_password", "") + + if not old_password or not new_password: + raise HTTPException(400, "ΠΠ°ΠΏΠΎΠ»Π½ΠΈΡΠ΅ Π²ΡΠ΅ ΠΏΠΎΠ»Ρ") + + if len(new_password) < 6: + raise HTTPException(400, "ΠΠΎΠ²ΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±ΡΡΡ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ 6 ΡΠΈΠΌΠ²ΠΎΠ»ΠΎΠ²") + + users = load_users() + + # ΠΡΠΎΠ²Π΅ΡΡΠ΅ΠΌ ΡΡΠ°ΡΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ + if not verify_password(old_password, users[user["username"]]["password"]): + raise HTTPException(400, "ΠΠ΅Π²Π΅ΡΠ½ΡΠΉ ΡΡΠ°ΡΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ") + + # Π£ΡΡΠ°Π½Π°Π²Π»ΠΈΠ²Π°Π΅ΠΌ Π½ΠΎΠ²ΡΠΉ ΠΏΠ°ΡΠΎΠ»Ρ + users[user["username"]]["password"] = get_password_hash(new_password) + save_users(users) + + return {"message": "ΠΠ°ΡΠΎΠ»Ρ ΠΈΠ·ΠΌΠ΅Π½ΡΠ½"} + +@app.get("/api/profile/stats") +async def get_profile_stats(user: dict = Depends(get_current_user)): + """ΠΠΎΠ»ΡΡΠΈΡΡ ΡΡΠ°ΡΠΈΡΡΠΈΠΊΡ ΠΏΡΠΎΡΠΈΠ»Ρ""" + users = load_users() + user_data = users.get(user["username"], {}) + + # ΠΠΎΠ΄ΡΡΠΈΡΡΠ²Π°Π΅ΠΌ ΡΠ΅ΡΠ²Π΅ΡΡ ΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΠ΅Π»Ρ + owned_servers = [] + accessible_servers = [] + + for server_dir in SERVERS_DIR.iterdir(): + if server_dir.is_dir(): + config = load_server_config(server_dir.name) + if config.get("owner") == user["username"]: + owned_servers.append({ + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name) + }) + elif user["username"] in user_data.get("servers", []) or user["role"] == "admin": + accessible_servers.append({ + "name": server_dir.name, + "displayName": config.get("displayName", server_dir.name) + }) + + # ΠΠΎΠ΄ΡΡΠΈΡΡΠ²Π°Π΅ΠΌ ΡΠΈΠΊΠ΅ΡΡ + tickets = load_tickets() + user_tickets = [t for t in tickets.values() if t["author"] == user["username"]] + + tickets_stats = { + "total": len(user_tickets), + "pending": len([t for t in user_tickets if t["status"] == "pending"]), + "in_progress": len([t for t in user_tickets if t["status"] == "in_progress"]), + "closed": len([t for t in user_tickets if t["status"] == "closed"]) + } + + return { + "username": user["username"], + "role": user["role"], + "owned_servers": owned_servers, + "accessible_servers": accessible_servers, + "tickets": tickets_stats, + "total_servers": len(owned_servers) + len(accessible_servers) + } + # API Π΄Π»Ρ ΡΠ΅ΡΠ²Π΅ΡΠΎΠ² @app.get("/api/servers") async def get_servers(user: dict = Depends(get_current_user)): diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index aa2fd33..39dd9a3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X, MessageSquare } from 'lucide-react'; +import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X, MessageSquare, UserCircle } from 'lucide-react'; import Console from './components/Console'; import FileManager from './components/FileManager'; import Stats from './components/Stats'; @@ -7,6 +7,7 @@ import ServerSettings from './components/ServerSettings'; import CreateServerModal from './components/CreateServerModal'; import Users from './components/Users'; import Tickets from './components/Tickets'; +import Profile from './components/Profile'; import Auth from './components/Auth'; import ErrorBoundary from './components/ErrorBoundary'; import ThemeSelector from './components/ThemeSelector'; @@ -23,6 +24,7 @@ function App() { const [showCreateModal, setShowCreateModal] = useState(false); const [showUsers, setShowUsers] = useState(false); const [showTickets, setShowTickets] = useState(false); + const [showProfile, setShowProfile] = useState(false); const [connectionError, setConnectionError] = useState(false); const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark'); const [sidebarOpen, setSidebarOpen] = useState(true); @@ -97,6 +99,12 @@ function App() { localStorage.setItem('theme', newTheme); }; + const handleUsernameChange = (newToken, newUsername) => { + setToken(newToken); + setUser({ ...user, username: newUsername }); + loadServers(); + }; + const startServer = async (serverName) => { try { const response = await axios.post( @@ -235,6 +243,56 @@ function App() { ); } + if (showProfile) { + return ( +
Π£ΠΏΡΠ°Π²Π»Π΅Π½ΠΈΠ΅ ΡΠ΅ΡΠ²Π΅ΡΠ°ΠΌΠΈ
+