Initial commit of CoproAPI
This commit is contained in:
commit
6521add891
|
|
@ -0,0 +1,4 @@
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
database.json
|
||||||
|
objects.json
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Universe of Bad Code - Секретный Бункер</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: monospace; background: #1a1a1a; color: #00ff00; padding: 20px; }
|
||||||
|
input, textarea { background: #333; color: #fff; border: 1px solid #00ff00; padding: 5px; margin: 5px 0; width: 300px; }
|
||||||
|
button { background: #00ff00; color: #000; border: none; padding: 5px 10px; cursor: pointer; margin-right: 5px; font-weight: bold; }
|
||||||
|
.tab-btn { background: #444; color: #fff; }
|
||||||
|
.tab-btn.active { background: #00ff00; color: #000; }
|
||||||
|
.hidden { display: none; }
|
||||||
|
.tab-content { border: 1px solid #00ff00; padding: 20px; margin-top: 10px; background: #222; }
|
||||||
|
|
||||||
|
/* Стили для сворачивания */
|
||||||
|
.item-box { border: 1px solid #555; padding: 10px; margin-bottom: 10px; background: #2b2b2b; }
|
||||||
|
.item-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-weight: bold; background: #333; padding: 5px; }
|
||||||
|
.item-body { padding-top: 10px; border-top: 1px dashed #555; margin-top: 5px; }
|
||||||
|
.toggle-icon { color: #00ff00; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="auth-block">
|
||||||
|
<h2>Вход в секретный бункер</h2>
|
||||||
|
<input type="text" id="username" placeholder="Логин"><br>
|
||||||
|
<input type="password" id="password" placeholder="Пароль"><br>
|
||||||
|
<button onclick="auth('login')">Войти</button>
|
||||||
|
<button onclick="auth('register')">Рега</button>
|
||||||
|
<p id="auth-response"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main-block" class="hidden">
|
||||||
|
<h2>Система управления бункером. Привет, <span id="user-display"></span>! <button onclick="logout()" style="background:#ff0000; color:#fff;">Выйти</button></h2>
|
||||||
|
|
||||||
|
<div id="tabs-header">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('profile')">Профиль</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('objects')">Объекты</button>
|
||||||
|
<button class="tab-btn hidden" id="admin-users-btn" onclick="switchTab('admin-users')">Все Профили (ROOT)</button>
|
||||||
|
<button class="tab-btn hidden" id="admin-objects-btn" onclick="switchTab('admin-objects')">Все Объекты (ROOT)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-profile" class="tab-content">
|
||||||
|
<h3>Твой Профиль</h3>
|
||||||
|
<div class="item-box">
|
||||||
|
<div class="item-header" onclick="toggleCollapse('prof-body', this)">
|
||||||
|
<span>Пользователь: <span id="prof-username"></span></span>
|
||||||
|
<span class="toggle-icon">[Развернуть]</span>
|
||||||
|
</div>
|
||||||
|
<div id="prof-body" class="item-body hidden">
|
||||||
|
<p><strong>Argon2 Хэш пароля в БД:</strong></p>
|
||||||
|
<textarea id="prof-hash" rows="2" readonly style="width:100%; color:#ff00ff;"></textarea>
|
||||||
|
|
||||||
|
<p><strong>Сменить пароль:</strong></p>
|
||||||
|
<input type="password" id="new-password-input" placeholder="Новый пароль"><br>
|
||||||
|
<button onclick="changePassword()">Сохранить новый пароль</button>
|
||||||
|
<span id="change-pwd-msg"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-objects" class="tab-content hidden">
|
||||||
|
<h3>Мои Созданные Объекты</h3>
|
||||||
|
<input type="text" id="new-obj-title" placeholder="Название объекта"><br>
|
||||||
|
<textarea id="new-obj-text" placeholder="Введите текст объекта..." rows="3"></textarea><br>
|
||||||
|
<button onclick="createObject()">Создать объект</button>
|
||||||
|
<div id="objects-list" style="margin-top:20px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-admin-users" class="tab-content hidden">
|
||||||
|
<h3>Админка БД: Текстовый дамп database.json</h3>
|
||||||
|
<div id="admin-users-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-admin-objects" class="tab-content hidden">
|
||||||
|
<h3>Админка БД: Все объекты из objects.json</h3>
|
||||||
|
<div id="admin-objects-list"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const API_URL = "https://universeofbadcode.online/api";
|
||||||
|
|
||||||
|
async function hashPassword(password) {
|
||||||
|
const msgBuffer = new TextEncoder().encode(password);
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
||||||
|
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeaders() {
|
||||||
|
return { 'Content-Type': 'application/json', 'X-Token': localStorage.getItem('token') };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция сворачивания/разворачивания блоков
|
||||||
|
function toggleCollapse(id, headerEl) {
|
||||||
|
const body = document.getElementById(id);
|
||||||
|
const icon = headerEl.querySelector('.toggle-icon');
|
||||||
|
if (body.classList.contains('hidden')) {
|
||||||
|
body.classList.remove('hidden');
|
||||||
|
icon.innerText = "[Свернуть]";
|
||||||
|
} else {
|
||||||
|
body.classList.add('hidden');
|
||||||
|
icon.innerText = "[Развернуть]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function auth(action) {
|
||||||
|
const user = document.getElementById('username').value;
|
||||||
|
const pass = document.getElementById('password').value;
|
||||||
|
const resElement = document.getElementById('auth-response');
|
||||||
|
if(!user || !pass) return;
|
||||||
|
|
||||||
|
const hashedPassword = await hashPassword(pass);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/${action}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username: user, password: hashedPassword })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (response.ok) {
|
||||||
|
if(action === 'login') {
|
||||||
|
localStorage.setItem('username', data.username);
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
initSystem();
|
||||||
|
} else {
|
||||||
|
resElement.innerText = "Рега успешна. Жми Войти.";
|
||||||
|
}
|
||||||
|
} else { resElement.innerText = "Ошибка: " + data.detail; }
|
||||||
|
} catch (err) { resElement.innerText = "Бэк упал."; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function initSystem() {
|
||||||
|
const username = localStorage.getItem('username');
|
||||||
|
if (!username) return;
|
||||||
|
|
||||||
|
document.getElementById('auth-block').classList.add('hidden');
|
||||||
|
document.getElementById('main-block').classList.remove('hidden');
|
||||||
|
document.getElementById('user-display').innerText = username;
|
||||||
|
|
||||||
|
if (username === 'root') {
|
||||||
|
document.getElementById('admin-users-btn').classList.remove('hidden');
|
||||||
|
document.getElementById('admin-objects-btn').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
document.getElementById('admin-users-btn').classList.add('hidden');
|
||||||
|
document.getElementById('admin-objects-btn').classList.add('hidden');
|
||||||
|
}
|
||||||
|
switchTab('profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
localStorage.clear();
|
||||||
|
document.getElementById('main-block').classList.add('hidden');
|
||||||
|
document.getElementById('auth-block').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePassword() {
|
||||||
|
const newPwd = document.getElementById('new-password-input').value;
|
||||||
|
const msgEl = document.getElementById('change-pwd-msg');
|
||||||
|
if(!newPwd) return;
|
||||||
|
|
||||||
|
const hashedNewPwd = await hashPassword(newPwd);
|
||||||
|
const res = await fetch(`${API_URL}/change-password`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getHeaders(),
|
||||||
|
body: JSON.stringify({ new_password: hashedNewPwd })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if(res.ok) {
|
||||||
|
msgEl.innerText = "Пароль изменен!";
|
||||||
|
msgEl.style.color = "#00ff00";
|
||||||
|
switchTab('profile');
|
||||||
|
} else {
|
||||||
|
msgEl.innerText = "Ошибка";
|
||||||
|
msgEl.style.color = "#ff0000";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function switchTab(tabName) {
|
||||||
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
||||||
|
|
||||||
|
document.getElementById(`tab-${tabName}`).classList.remove('hidden');
|
||||||
|
if(event) event.target.classList.add('active');
|
||||||
|
|
||||||
|
if (tabName === 'profile') {
|
||||||
|
const res = await fetch(`${API_URL}/profile`, { headers: getHeaders() });
|
||||||
|
const data = await res.json();
|
||||||
|
document.getElementById('prof-username').innerText = data.username;
|
||||||
|
document.getElementById('prof-hash').value = data.hash;
|
||||||
|
// Сбрасываем состояние скрытия на дефолтное
|
||||||
|
document.getElementById('prof-body').classList.add('hidden');
|
||||||
|
document.querySelector('#tab-profile .toggle-icon').innerText = "[Развернуть]";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabName === 'objects') { loadUserObjects(); }
|
||||||
|
|
||||||
|
if (tabName === 'admin-users') {
|
||||||
|
const res = await fetch(`${API_URL}/admin/users`, { headers: getHeaders() });
|
||||||
|
const users = await res.json();
|
||||||
|
document.getElementById('admin-users-list').innerHTML = users.map((u, index) => `
|
||||||
|
<div class="item-box">
|
||||||
|
<div class="item-header" onclick="toggleCollapse('admin-u-${index}', this)">
|
||||||
|
<span>Пользователь: ${u.username}</span>
|
||||||
|
<span class="toggle-icon">[Развернуть]</span>
|
||||||
|
</div>
|
||||||
|
<div id="admin-u-${index}" class="item-body hidden">
|
||||||
|
<b>Хэш Argon2:</b> <span style="color:#ff00ff;">${u.password}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabName === 'admin-objects') {
|
||||||
|
const res = await fetch(`${API_URL}/admin/objects`, { headers: getHeaders() });
|
||||||
|
const objs = await res.json();
|
||||||
|
document.getElementById('admin-objects-list').innerHTML = objs.map((o, index) => `
|
||||||
|
<div class="item-box">
|
||||||
|
<div class="item-header" onclick="toggleCollapse('admin-o-${index}', this)">
|
||||||
|
<span>Название: ${o.title} (Автор: ${o.username})</span>
|
||||||
|
<span class="toggle-icon">[Развернуть]</span>
|
||||||
|
</div>
|
||||||
|
<div id="admin-o-${index}" class="item-body hidden">
|
||||||
|
<b>Текст:</b><br>${o.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUserObjects() {
|
||||||
|
const res = await fetch(`${API_URL}/objects`, { headers: getHeaders() });
|
||||||
|
const objs = await res.json();
|
||||||
|
document.getElementById('objects-list').innerHTML = objs.map((o, index) => `
|
||||||
|
<div class="item-box">
|
||||||
|
<div class="item-header" onclick="toggleCollapse('user-o-${index}', this)">
|
||||||
|
<span>Название: ${o.title}</span>
|
||||||
|
<span class="toggle-icon">[Развернуть]</span>
|
||||||
|
</div>
|
||||||
|
<div id="user-o-${index}" class="item-body hidden">
|
||||||
|
<b>Текст:</b><br>${o.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createObject() {
|
||||||
|
const title = document.getElementById('new-obj-title').value;
|
||||||
|
const text = document.getElementById('new-obj-text').value;
|
||||||
|
if(!title || !text) return;
|
||||||
|
await fetch(`${API_URL}/objects`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getHeaders(),
|
||||||
|
body: JSON.stringify({ title: title, text: text })
|
||||||
|
});
|
||||||
|
document.getElementById('new-obj-title').value = '';
|
||||||
|
document.getElementById('new-obj-text').value = '';
|
||||||
|
loadUserObjects();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(localStorage.getItem('username')) { initSystem(); }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
from fastapi import FastAPI, HTTPException, Header
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from argon2 import PasswordHasher
|
||||||
|
from argon2.exceptions import VerifyMismatchError
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
DB_FILE = "database.json"
|
||||||
|
OBJ_FILE = "objects.json"
|
||||||
|
ph = PasswordHasher()
|
||||||
|
SESSIONS = {}
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class ChangePasswordModel(BaseModel):
|
||||||
|
new_password: str
|
||||||
|
|
||||||
|
class NoteObject(BaseModel):
|
||||||
|
title: str # Добавили название
|
||||||
|
text: str
|
||||||
|
|
||||||
|
def read_json(filename):
|
||||||
|
if not os.path.exists(filename): return []
|
||||||
|
with open(filename, "r") as f:
|
||||||
|
try: return json.load(f)
|
||||||
|
except json.JSONDecodeError: return []
|
||||||
|
|
||||||
|
def write_json(filename, data):
|
||||||
|
with open(filename, "w") as f: json.dump(data, f, indent=4)
|
||||||
|
|
||||||
|
def get_user_from_token(token: str):
|
||||||
|
if not token or token not in SESSIONS:
|
||||||
|
raise HTTPException(status_code=401, detail="Токен невалидный")
|
||||||
|
return SESSIONS[token]
|
||||||
|
|
||||||
|
@app.post("/register")
|
||||||
|
def register(user: User):
|
||||||
|
db = read_json(DB_FILE)
|
||||||
|
if any(u["username"] == user.username for u in db):
|
||||||
|
raise HTTPException(status_code=400, detail="Такой юзер уже есть")
|
||||||
|
db.append({"username": user.username, "password": ph.hash(user.password)})
|
||||||
|
write_json(DB_FILE, db)
|
||||||
|
return {"message": "Регистрация успешна"}
|
||||||
|
|
||||||
|
@app.post("/login")
|
||||||
|
def login(user: User):
|
||||||
|
db = read_json(DB_FILE)
|
||||||
|
for u in db:
|
||||||
|
if u["username"] == user.username:
|
||||||
|
try:
|
||||||
|
ph.verify(u["password"], user.password)
|
||||||
|
session_token = secrets.token_hex(32)
|
||||||
|
SESSIONS[session_token] = user.username
|
||||||
|
return {"message": "Успешный вход", "token": session_token, "username": user.username}
|
||||||
|
except VerifyMismatchError:
|
||||||
|
raise HTTPException(status_code=401, detail="Неверный пароль")
|
||||||
|
raise HTTPException(status_code=401, detail="Юзер не найден")
|
||||||
|
|
||||||
|
@app.get("/profile")
|
||||||
|
def get_profile(x_token: str = Header(None)):
|
||||||
|
username = get_user_from_token(x_token)
|
||||||
|
db = read_json(DB_FILE)
|
||||||
|
for u in db:
|
||||||
|
if u["username"] == username:
|
||||||
|
return {"username": u["username"], "hash": u["password"]}
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
# Эндпоинт для смены пароля (любой юзер меняет свой, root в админке меняет тоже свой, т.к. сессия его)
|
||||||
|
@app.post("/change-password")
|
||||||
|
def change_password(data: ChangePasswordModel, x_token: str = Header(None)):
|
||||||
|
username = get_user_from_token(x_token)
|
||||||
|
db = read_json(DB_FILE)
|
||||||
|
for u in db:
|
||||||
|
if u["username"] == username:
|
||||||
|
u["password"] = ph.hash(data.new_password)
|
||||||
|
write_json(DB_FILE, db)
|
||||||
|
return {"message": "Пароль успешно изменен"}
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
@app.post("/objects")
|
||||||
|
def create_object(obj: NoteObject, x_token: str = Header(None)):
|
||||||
|
username = get_user_from_token(x_token)
|
||||||
|
objects = read_json(OBJ_FILE)
|
||||||
|
objects.append({"username": username, "title": obj.title, "text": obj.text})
|
||||||
|
write_json(OBJ_FILE, objects)
|
||||||
|
return {"message": "Объект создан"}
|
||||||
|
|
||||||
|
@app.get("/objects")
|
||||||
|
def get_objects(x_token: str = Header(None)):
|
||||||
|
username = get_user_from_token(x_token)
|
||||||
|
objects = read_json(OBJ_FILE)
|
||||||
|
return [o for o in objects if o["username"] == username]
|
||||||
|
|
||||||
|
@app.get("/admin/objects")
|
||||||
|
def admin_get_objects(x_token: str = Header(None)):
|
||||||
|
username = get_user_from_token(x_token)
|
||||||
|
if username != "root": raise HTTPException(status_code=403)
|
||||||
|
return read_json(OBJ_FILE)
|
||||||
|
|
||||||
|
@app.get("/admin/users")
|
||||||
|
def admin_get_users(x_token: str = Header(None)):
|
||||||
|
username = get_user_from_token(x_token)
|
||||||
|
if username != "root": raise HTTPException(status_code=403)
|
||||||
|
return read_json(DB_FILE)
|
||||||
Loading…
Reference in New Issue