diff --git a/.gitignore b/.gitignore index 9c86c17..cbde85d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,2 @@ .venv -.env -API/__pycache__ -API/models/__pycache__ -API/routers/__pycache__ -API/schemas/__pycache__ -API/utils/__pycache__ +configs/config/params_spectralvpn.conf \ No newline at end of file diff --git a/API/Dockerfile b/API/Dockerfile deleted file mode 100644 index 338fccb..0000000 --- a/API/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM python:3.14.4-slim -WORKDIR /app -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY . . -EXPOSE 8000 -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/API/main.py b/API/main.py index 8d013fd..e4fb7b0 100644 --- a/API/main.py +++ b/API/main.py @@ -1,24 +1,109 @@ -from datetime import datetime -from contextlib import asynccontextmanager -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware -from utils.database import engine -import models -from routers import server, user, config +from fastapi import FastAPI, HTTPException, Query, status +from pydantic import BaseModel, EmailStr +import configparser +import utils -@asynccontextmanager -async def lifespan(app: FastAPI): - async with engine.begin() as conn: - await conn.run_sync(models.Base.metadata.create_all) - yield - await engine.dispose() +app = FastAPI() -app = FastAPI(lifespan=lifespan) +config = configparser.ConfigParser() +config.read('params.conf', encoding='utf-8') +args = [ + config["api"]["path"], + config["api"]["host"], + config["api"]["username"], + config["api"]["password"], + config.getint("api", "inbaund_id"), + config["api"]["inbaund_url"] + ] -app.include_router(server.router) -app.include_router(user.router) -app.include_router(config.router) +class RegisterBody(BaseModel) : + email : EmailStr + password : str -@app.get("/ping") -async def ping(): - return {"message": "pong", "time": datetime.now().timestamp()} +class LoginBody(BaseModel) : + email : EmailStr + password : str + +class UrlsBody(BaseModel) : + urls_name : str + email : EmailStr + password : str + +@app.post("/registration", status_code=status.HTTP_201_CREATED) +def registration(body: RegisterBody) : + methods = utils.API(*args) + result = methods.registration(body.email, body.password) + methods.close() + if result["code"] == 1: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email is busy." + ) + return result["data"] + +@app.post("/login", status_code=status.HTTP_200_OK) +def login(body: LoginBody) : + methods = utils.API(*args) + answer = methods.login(body.email, body.password) + methods.close() + if answer["code"] == 1 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect email or password." + ) + else : + return answer["data"] + +@app.post("/add_url", status_code=status.HTTP_200_OK) +def add_url(body: UrlsBody) : + methods = utils.API(*args) + answer = methods.add_url(body.email, body.password, body.urls_name) + methods.close() + if answer["code"] == 1 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect email or password." + ) + elif answer["code"] == 2 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The url already exists." + ) + else : + return answer["data"] + +@app.get("/get_url", status_code=status.HTTP_200_OK) +def get_url(email: EmailStr = Query(...), password: str = Query(...), urls_name: str = Query(...)) : + methods = utils.API(*args) + answer = methods.get_url(email, password, urls_name) + methods.close() + if answer["code"] == 1 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect email or password." + ) + elif answer["code"] == 2 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The url does not exist" + ) + else : + return answer["data"] + +@app.delete("/del_url", status_code=status.HTTP_200_OK) +def del_url(email: EmailStr = Query(...), password: str = Query(...), urls_name: str = Query(...)) : + methods = utils.API(*args) + answer = methods.del_url(email, password, urls_name) + methods.close() + if answer["code"] == 1 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Incorrect email or password." + ) + elif answer["code"] == 2 : + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="The url does not exist" + ) + else : + return answer["data"] diff --git a/API/models/__init__.py b/API/models/__init__.py deleted file mode 100644 index 723b0f7..0000000 --- a/API/models/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .base import Base -from .user import User -from .server import Server -from .token import AuthToken -from .config import Config - -__all__ = ["Base", "User", "Server", "AuthToken", "Config"] \ No newline at end of file diff --git a/API/models/base.py b/API/models/base.py deleted file mode 100644 index 2278416..0000000 --- a/API/models/base.py +++ /dev/null @@ -1,4 +0,0 @@ -from sqlalchemy.orm import DeclarativeBase - -class Base(DeclarativeBase): - pass \ No newline at end of file diff --git a/API/models/config.py b/API/models/config.py deleted file mode 100644 index 5439141..0000000 --- a/API/models/config.py +++ /dev/null @@ -1,14 +0,0 @@ -from datetime import datetime -from sqlalchemy import String, DateTime, ForeignKey -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.sql import func -from .base import Base - -class Config(Base): - __tablename__ = "configs" - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) - name: Mapped[str] = mapped_column(String(64), nullable=False) - config: Mapped[str] = mapped_column(String(1024), nullable=False) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) - deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) diff --git a/API/models/server.py b/API/models/server.py deleted file mode 100644 index e371997..0000000 --- a/API/models/server.py +++ /dev/null @@ -1,19 +0,0 @@ -from datetime import datetime -from sqlalchemy import String, DateTime -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.sql import func -from .base import Base - -class Server(Base): - __tablename__ = "servers" - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) - code: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True) - host: Mapped[str] = mapped_column(String(128), nullable=False) - port: Mapped[int] = mapped_column(nullable=False) - user: Mapped[str] = mapped_column(String(64), nullable=False) - password: Mapped[str] = mapped_column(String(64), nullable=False) - inbound_id: Mapped[int] = mapped_column(nullable=False) - version: Mapped[str] = mapped_column(String(16), nullable=False, default="stable") - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) - deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) diff --git a/API/models/token.py b/API/models/token.py deleted file mode 100644 index 5d05b09..0000000 --- a/API/models/token.py +++ /dev/null @@ -1,17 +0,0 @@ -from datetime import datetime -from sqlalchemy import String, DateTime, ForeignKey -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.sql import func -from .base import Base - -class AuthToken(Base): - __tablename__ = "tokens" - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"), nullable=False) - token_hash: Mapped[str] = mapped_column(String(512), index=True, nullable=False, unique=True) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) - expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) - revoked: Mapped[bool] = mapped_column(default=False, nullable=False) - revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) - def is_valid(self) -> bool: - return not self.revoked and self.expires_at > datetime.utcnow() diff --git a/API/models/user.py b/API/models/user.py deleted file mode 100644 index 70e6c4e..0000000 --- a/API/models/user.py +++ /dev/null @@ -1,16 +0,0 @@ -from datetime import datetime -from sqlalchemy import String, DateTime, ForeignKey -from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.sql import func -from .base import Base -from .server import Server - -class User(Base): - __tablename__ = "users" - id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - email: Mapped[str] = mapped_column(String(256), index=True, nullable=False) - pass_hash: Mapped[str] = mapped_column(String(512), nullable=False) - server_id: Mapped[int] = mapped_column(ForeignKey("servers.id")) - created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), nullable=False) - deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) - server: Mapped[Server] = relationship("Server", lazy="selectin") diff --git a/API/requirements.txt b/API/requirements.txt deleted file mode 100644 index e531270..0000000 --- a/API/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -fastapi[all] -sqlalchemy -psycopg2-binary -asyncpg -passlib -bcrypt==4.3.0 -httpx diff --git a/API/routers/config.py b/API/routers/config.py deleted file mode 100644 index 27c91c3..0000000 --- a/API/routers/config.py +++ /dev/null @@ -1,132 +0,0 @@ -from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy import select -from sqlalchemy.orm import selectinload -from models.user import User -from models.config import Config -from schemas.config import ConfigCreate, ConfigResponse, ConfigListResponse, ConfigDelete -from utils.auth import get_current_user -from utils.database import get_db -from utils.xui import XUIClient - -router = APIRouter(prefix="/config") - -@router.post("/add", status_code=201) -async def create_config(body: ConfigCreate, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): - existing = await db.execute( - select(Config).where( - Config.user_id == current_user.id, - Config.name == body.name, - Config.deleted_at.is_(None) - ) - ) - if existing.scalar_one_or_none(): - raise HTTPException( - status_code=409, - detail="Config name is invalid" - ) - result = await db.execute( - select(User) - .where(User.id == current_user.id) - .options(selectinload(User.server)) - ) - user_with_server = result.scalar_one() - server = user_with_server.server - if not server or server.deleted_at is not None: - raise HTTPException( - status_code=404, - detail="The server not found" - ) - xui = await XUIClient.from_server(server) - client_email_xui = f"{current_user.email}-{body.name}" - display_name = f"SpectralVPN-{body.name}" - config_url = await xui.add_client( - client_email=client_email_xui, - display_name=display_name - ) - new_config = Config( - user_id=current_user.id, - name=body.name, - config=config_url - ) - db.add(new_config) - await db.commit() - await db.refresh(new_config) - return ConfigResponse( - name=new_config.name, - config=new_config.config, - created_at=new_config.created_at, - bytes_used=0 - ) - -@router.get("/get_info") -async def get_configs(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): - result = await db.execute( - select(Config).where( - Config.user_id == current_user.id, - Config.deleted_at.is_(None) - ).order_by(Config.created_at.desc()) - ) - configs_db = result.scalars().all() - configs = [] - xui = None - for cfg in configs_db: - bytes_used = 0 - try: - if xui is None: - server_result = await db.execute( - select(User) - .where(User.id == current_user.id) - .options(selectinload(User.server)) - ) - server = server_result.scalar_one().server - xui = await XUIClient.from_server(server) - client_email = f"{current_user.email}-{cfg.name}" - bytes_used = await xui.get_client_traffic(client_email) - except: - pass - configs.append( - ConfigResponse( - name=cfg.name, - config=cfg.config, - created_at=cfg.created_at, - bytes_used=bytes_used - ) - ) - return ConfigListResponse(configs=configs) - -@router.delete("/delete", status_code=204) -async def delete_config(body: ConfigDelete, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): - result = await db.execute( - select(Config).where( - Config.user_id == current_user.id, - Config.name == body.name, - Config.deleted_at.is_(None) - ) - ) - config_record = result.scalar_one_or_none() - if not config_record: - raise HTTPException( - status_code=404, - detail="Config not found" - ) - try: - server_result = await db.execute( - select(User) - .where(User.id == current_user.id) - .options(selectinload(User.server)) - ) - server = server_result.scalar_one().server - if server: - xui = await XUIClient.from_server(server) - client_email_xui = f"{current_user.email}-{body.name}" - await xui.delete_client(client_email_xui) - except: - HTTPException( - status_code=500, - detail="Server error" - ) - config_record.deleted_at = datetime.utcnow() - await db.commit() - return None diff --git a/API/routers/server.py b/API/routers/server.py deleted file mode 100644 index 3369b67..0000000 --- a/API/routers/server.py +++ /dev/null @@ -1,57 +0,0 @@ -from datetime import datetime -from fastapi import APIRouter, Request, Depends, HTTPException -from sqlalchemy import select, update -from sqlalchemy.ext.asyncio import AsyncSession -from models.server import Server -from schemas.server import ServerAdd, ServerDel, ServerInfo -from utils.database import get_db - -router = APIRouter(prefix="/server") - -@router.post("/add", status_code=201) -async def add_server(body: ServerAdd, request: Request, db: AsyncSession = Depends(get_db)): - existing = await db.execute( - select(Server).where( - (Server.name == body.name) | - (Server.code == body.code) - ) - ) - if existing.scalar_one_or_none(): - raise HTTPException( - status_code=409, - detail="Nameor code is already exists" - ) - new_server = Server( - name=body.name, - code=body.code, - host=body.host, - port=body.port, - user=body.user, - password=body.password, - inbound_id=body.inbound_id, - version=body.version - ) - db.add(new_server) - await db.commit() - await db.refresh(new_server) - return ServerInfo.model_validate(new_server, from_attributes=True) - -@router.delete("/delete", status_code=204) -async def del_server(body: ServerDel, db: AsyncSession = Depends(get_db)): - result = await db.execute( - select(Server).where( - (Server.name == body.name) & - (Server.user == body.user) & - (Server.password == body.password) & - (Server.deleted_at.is_(None)) - ) - ) - server = result.scalar_one_or_none() - if not server: - raise HTTPException( - status_code=404, - detail="Server not found, or invalid secure_key" - ) - server.deleted_at = datetime.utcnow() - await db.commit() - return None \ No newline at end of file diff --git a/API/routers/user.py b/API/routers/user.py deleted file mode 100644 index 340ffb3..0000000 --- a/API/routers/user.py +++ /dev/null @@ -1,144 +0,0 @@ -from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy import select, update -from sqlalchemy.ext.asyncio import AsyncSession -from schemas.user import UserCreate, UserResponse, UserLogin, UserTokenRevoke -from models.server import Server -from models.user import User -from models.config import Config -from models.token import AuthToken -from utils.database import get_db -from utils.auth import get_password_hash, create_token, verify_password, get_current_user, hash_token -from utils.xui import XUIClient - -router = APIRouter(prefix="/user") - -@router.post("/signup", status_code=201) -async def signup(body: UserCreate, db: AsyncSession = Depends(get_db)): - promo_code = (body.promo_code or "").strip() - result = await db.execute( - select(Server).where( - Server.deleted_at.is_(None), - Server.code == promo_code - ) - ) - server = result.scalar_one_or_none() - if not server: - raise HTTPException( - status_code=404, - detail="Promo-code is not valid" - ) - existing = await db.execute( - select(User).where( - User.email == body.email, - User.deleted_at.is_(None) - ) - ) - if existing.scalar_one_or_none(): - raise HTTPException( - status_code=409, - detail="Email is busy" - ) - new_user = User( - email=body.email, - pass_hash= get_password_hash(body.password), - server_id=server.id - ) - db.add(new_user) - await db.commit() - await db.refresh(new_user) - access_token = await create_token(user_id=new_user.id, db=db) - return UserResponse( - id=new_user.id, - email=new_user.email, - created_at=new_user.created_at, - access_token=access_token - ) - -@router.post("/login", status_code=200) -async def login(body: UserLogin, db: AsyncSession = Depends(get_db)): - result = await db.execute( - select(User).where(User.email == body.email) - ) - user = result.scalar_one_or_none() - if not user or user.deleted_at is not None: - raise HTTPException( - status_code=401, - detail="Incorrect email or password" - ) - if not await verify_password(body.password, user.pass_hash, user.id, db): - raise HTTPException( - status_code=401, - detail="Incorrect email or password" - ) - access_token = await create_token(user_id=user.id, db=db) - return UserResponse( - id=user.id, - email=user.email, - created_at=user.created_at, - access_token=access_token - ) - -@router.post("/logout", status_code=200) -async def revoke_token(body: UserTokenRevoke, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): - token_to_revoke = body.token_to_revoke.strip() - token_hash = hash_token(token_to_revoke) - result = await db.execute( - select(AuthToken).where( - AuthToken.token_hash == token_hash, - AuthToken.user_id == current_user.id - ) - ) - token_record = result.scalar_one_or_none() - if not token_record: - raise HTTPException( - status_code=404, - detail="Token not found" - ) - if token_record.revoked: - return {"message": "Token already revoked"} - token_record.revoked = True - token_record.revoked_at = datetime.utcnow() - await db.commit() - return {"message": "Token successfully revoked"} - -@router.delete("/delete", status_code=200) -async def delete_user(current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db)): - if current_user.deleted_at is not None: - raise HTTPException( - status_code=400, - detail="User is already deleted" - ) - try: - await db.refresh(current_user, ["server"]) - server = current_user.server - if server and server.inbound_id: - xui = await XUIClient.from_server(server) - result = await db.execute( - select(Config).where( - Config.user_id == current_user.id, - Config.deleted_at.is_(None) - ) - ) - user_configs = result.scalars().all() - for cfg in user_configs: - client_email_xui = f"{current_user.email}-{cfg.name}" - try: - await xui.delete_client(server.inbound_id, client_email_xui) - except: - pass - except: - pass - current_user.deleted_at = datetime.utcnow() - await db.execute( - update(AuthToken) - .where(AuthToken.user_id == current_user.id) - .values(revoked=True, revoked_at=datetime.utcnow()) - ) - await db.execute( - update(Config) - .where(Config.user_id == current_user.id, Config.deleted_at.is_(None)) - .values(deleted_at=datetime.utcnow()) - ) - await db.commit() - return {"message": "User has been successfully deleted"} diff --git a/API/schemas/config.py b/API/schemas/config.py deleted file mode 100644 index 50cbea5..0000000 --- a/API/schemas/config.py +++ /dev/null @@ -1,20 +0,0 @@ -from datetime import datetime -from pydantic import BaseModel, Field - -class ConfigCreate(BaseModel): - name: str = Field(..., max_length=64) - -class ConfigResponse(BaseModel): - name: str - config: str - created_at: datetime - bytes_used: int = 0 - - class Config: - from_attributes = True - -class ConfigDelete(BaseModel): - name: str - -class ConfigListResponse(BaseModel): - configs: list[ConfigResponse] diff --git a/API/schemas/server.py b/API/schemas/server.py deleted file mode 100644 index f9ff915..0000000 --- a/API/schemas/server.py +++ /dev/null @@ -1,25 +0,0 @@ -from datetime import datetime -from typing import Literal -from pydantic import BaseModel, Field - -class ServerBase(BaseModel): - name: str = Field(max_length=64) - user: str = Field(max_length=64) - password: str = Field(max_length=64) - -class ServerAdd(ServerBase): - host: str = Field(max_length=128) - port: int = Field(ge=1, le=65536) - code: str = Field(max_length=128) - inbound_id: int - version: Literal["legacy", "stable"] - -class ServerDel(ServerBase): - pass - -class ServerInfo(BaseModel): - id: int - name: str = Field(max_length=64) - code: str - created_at: datetime - version: str diff --git a/API/schemas/user.py b/API/schemas/user.py deleted file mode 100644 index d9a21ff..0000000 --- a/API/schemas/user.py +++ /dev/null @@ -1,21 +0,0 @@ -from datetime import datetime -from pydantic import BaseModel, EmailStr, Field - -class UserCreate(BaseModel): - email: EmailStr = Field(max_length=256) - password: str = Field(min_length=6, max_length=128) - promo_code: str | None = Field(default="", max_length=128) - -class UserLogin(BaseModel): - email: EmailStr - password: str - -class UserTokenRevoke(BaseModel): - token_to_revoke: str - -class UserResponse(BaseModel): - id: int - email: EmailStr - created_at: datetime - access_token: str - token_type: str = "bearer" diff --git a/API/utils.py b/API/utils.py new file mode 100644 index 0000000..3b3ced0 --- /dev/null +++ b/API/utils.py @@ -0,0 +1,253 @@ +import sqlite3 +import pandas +import datetime +import hashlib +import json +import uuid +import requests +import random + +class API : + def __init__(self, path: str, host: str, username: str, passwd: str, inbaund_id: int, inbaund_url: str) : + self.con = sqlite3.connect(path) + self.cur = self.con.cursor() + self.cur.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL UNIQUE, + pass_hash TEXT NOT NULL, + register TEXT NOT NULL, + urls TEXT DEFAULT "[]" NOT NULL, + payment TEXT DEFAULT "" + ) + """) + self.con.commit() + + self.x_ui = requests.Session() + self.x_ui.verify = True + self.x_ui.headers.update({ + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (compatible; py3xui-like)" + }) + response = self.x_ui.post(f"{host}/login", json={ + "username": username, + "password": passwd + }) + response.raise_for_status() + self.host = host + + self.inbaund_id = inbaund_id + self.inbaund_url = inbaund_url + + def read_table(self, sql_command: str, params=None) -> dict : + if params == None : + params = () + return json.loads(pandas.read_sql(sql_command, self.con, params=params).to_json()) + + @staticmethod + def get_hash(text: str) -> str: + return hashlib.sha256(text.encode()).hexdigest() + + def response(self, method: str, endpoint: str, **kwargs) : + response = self.x_ui.request(method, f"{self.host}/panel/{endpoint}", **kwargs) + response.raise_for_status() + return response.json() + + def registration(self, email: str, passwd: str) -> dict : + """ + 0 - Success + 1 - Email is busy + """ + date = datetime.datetime.now() + try : + self.cur.execute(""" + INSERT INTO users (email, pass_hash, register) + VALUES (?, ?, ?) + """, (email, self.get_hash(passwd), f"{date.day}-{date.month}-{date.year} {date.hour}:{date.minute}:{date.second}")) + self.con.commit() + except : + return {"code": 1} + data = self.read_table(""" + SELECT id FROM users + WHERE email = ? + """, (email,)) + return {"code": 0, "data": {"id": int(data["id"]["0"]), "email": email}} + + def close(self) : + self.con.close() + self.x_ui.close() + + def login(self, email: str, passwd: str) -> dict : + """ + 0 - Success + 1 - Incorrect email or password + """ + data = self.read_table(""" + SELECT id, urls FROM users + WHERE email = ? AND pass_hash = ? + """, (email, self.get_hash(passwd))) + if data["id"] == {} : + return {"code": 1} + return {"code": 0, "data": {"id": int(data["id"]["0"]), "email": email, "urls": json.loads(data["urls"]["0"])}} + + def add_url(self, email: str, passwd: str, url_name: str) -> dict : + """ + 0 - Success + 1 - Incorrect email or password + 2 - The url already exists + """ + data = self.read_table(""" + SELECT id, urls FROM users + WHERE email = ? AND pass_hash = ? + """, (email, self.get_hash(passwd))) + if data["id"] == {} : + return {"code": 1} + urls = json.loads(data["urls"]["0"]) + if url_name in urls : + return {"code": 2} + + response = self.response("post", "inbound/list") + inbaund = None + for i in response["obj"] : + if i["id"] == self.inbaund_id : + inbaund = i + break + if inbaund == None : + raise TypeError + stream = json.loads(inbaund["streamSettings"]) + suid = random.choice(stream["realitySettings"]["shortIds"]) + sni = random.choice(stream["realitySettings"]["serverNames"]) + pbk = stream["realitySettings"]["settings"]["publicKey"] + uid = str(uuid.uuid4()) + + response = self.response("post", "inbound/addClient", json={ + "id": self.inbaund_id, + "settings": json.dumps({ + "clients":[{ + "id": uid, + "flow": "xtls-rprx-vision", + "email": f"{email}-{url_name}", + "limitIp": 0, + "totalGB": 0, + "expiryTime": 0, + "enable": True, + "tgId": "", + "subId": suid, + "comment": "", + "reset": 0 + }] + }) + }) + + response = self.response("post", "inbound/list") + inbaund = None + for i in response["obj"] : + if i["id"] == self.inbaund_id : + inbaund = i + break + if inbaund == None : + raise TypeError + + url = f"vless://{uid}@{self.inbaund_url}?security=reality&pbk={pbk}&fp=random&sni={sni}&sid={suid}&spx=%2F&flow=xtls-rprx-vision#SpectralVPN-{url_name}" + + urls.append(url_name) + + self.cur.execute(""" + UPDATE users SET + urls = ? + WHERE id = ? + """, (json.dumps(urls), int(data["id"]["0"]))) + self.con.commit() + data = self.read_table(""" + SELECT id, urls FROM users + WHERE email = ? AND pass_hash = ? + """, (email, self.get_hash(passwd))) + + return {"code": 0, "data": {"id": int(data["id"]["0"]), "email": email, "urls": json.loads(data["urls"]["0"]), "url": url}} + + def get_url(self, email: str, passwd: str, url_name: str) -> dict : + """ + 0 - Success + 1 - Incorrect email or password + 2 - The url does not exist + """ + data = self.read_table(""" + SELECT id, urls FROM users + WHERE email = ? AND pass_hash = ? + """, (email, self.get_hash(passwd))) + if data["id"] == {} : + return {"code": 1} + urls = json.loads(data["urls"]["0"]) + if not url_name in urls : + return {"code": 2} + + response = self.response("post", "inbound/list") + inbaund = None + for i in response["obj"] : + if i["id"] == self.inbaund_id : + inbaund = i + break + if inbaund == None : + raise TypeError + stream = json.loads(inbaund["streamSettings"]) + suid = random.choice(stream["realitySettings"]["shortIds"]) + sni = random.choice(stream["realitySettings"]["serverNames"]) + pbk = stream["realitySettings"]["settings"]["publicKey"] + client = None + for i in json.loads(inbaund["settings"])["clients"] : + if i["email"] == f"{email}-{url_name}" : + client = i + break + if client == None : + raise TypeError + uid = client["id"] + url = f"vless://{uid}@{self.inbaund_url}?security=reality&pbk={pbk}&fp=random&sni={sni}&sid={suid}&spx=%2F&flow=xtls-rprx-vision#SpectralVPN-{url_name}" + + return {"code": 0, "data": {"id": int(data["id"]["0"]), "email": email, "urls": urls, "url": url}} + + def del_url(self, email: str, passwd: str, url_name: str) -> dict : + """ + 0 - Success + 1 - Incorrect email or password + 2 - The url does not exist + """ + data = self.read_table(""" + SELECT id, urls FROM users + WHERE email = ? AND pass_hash = ? + """, (email, self.get_hash(passwd))) + if data["id"] == {} : + return {"code": 1} + urls = json.loads(data["urls"]["0"]) + if not url_name in urls : + return {"code": 2} + + response = self.response("post", "inbound/list") + inbaund = None + for i in response["obj"] : + if i["id"] == self.inbaund_id : + inbaund = i + break + if inbaund == None : + raise TypeError + client = None + for i in json.loads(inbaund["settings"])["clients"] : + if i["email"] == f"{email}-{url_name}" : + client = i + break + if client == None : + raise TypeError + uid = client["id"] + self.response("post", f"inbound/{self.inbaund_id}/delClient/{uid}") + + urls.remove(url_name) + self.cur.execute(""" + UPDATE users SET + urls = ? + WHERE id = ? + """, (json.dumps(urls), int(data["id"]["0"]))) + self.con.commit() + data = self.read_table(""" + SELECT id, urls FROM users + WHERE email = ? AND pass_hash = ? + """, (email, self.get_hash(passwd))) + return {"code": 0, "data": {"id": int(data["id"]["0"]), "email": email, "urls": urls}} \ No newline at end of file diff --git a/API/utils/auth.py b/API/utils/auth.py deleted file mode 100644 index a3f46ae..0000000 --- a/API/utils/auth.py +++ /dev/null @@ -1,74 +0,0 @@ -import secrets -import hashlib -from datetime import datetime, timedelta -from typing import Annotated -from fastapi import Depends, HTTPException, status -from fastapi.security import APIKeyHeader -from sqlalchemy import select, update -from sqlalchemy.ext.asyncio import AsyncSession -from passlib.context import CryptContext -from models.token import AuthToken -from models.user import User -from utils.database import get_db - -pwd_context = CryptContext(schemes=["bcrypt"]) -API_KEY_HEADER = APIKeyHeader(name="X-API-KEY", auto_error=False) - -def get_password_hash(password: str) -> str: - return pwd_context.hash(password) - -async def verify_password(plain_password: str, hashed_password: str, user_id: int, db: AsyncSession) -> bool: - if hashed_password != "Unknown": - return pwd_context.verify(plain_password, hashed_password) - new_hash = get_password_hash(plain_password) - await db.execute( - update(User) - .where(User.id == user_id) - .values(pass_hash=new_hash) - ) - await db.commit() - return True - -def hash_token(raw_token: str) -> str: - return hashlib.sha256(raw_token.encode('utf-8')).hexdigest() - -async def create_token(user_id: int, db: AsyncSession) -> str: - raw_token = secrets.token_urlsafe(48) - token_hash = hash_token(raw_token) - new_token = AuthToken( - user_id=user_id, - token_hash=token_hash, - expires_at=datetime.utcnow() + timedelta(days=30) - ) - db.add(new_token) - await db.commit() - await db.refresh(new_token) - return raw_token - -async def get_current_user(api_key: Annotated[str | None, Depends(API_KEY_HEADER)], db: AsyncSession = Depends(get_db)) -> User: - if not api_key: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token is not valid" - ) - token_hash = hash_token(api_key) - result = await db.execute( - select(AuthToken).where( - AuthToken.token_hash == token_hash, - AuthToken.revoked == False, - AuthToken.expires_at > datetime.utcnow() - ) - ) - token_record = result.scalar_one_or_none() - if not token_record: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Token is expired" - ) - user = await db.get(User, token_record.user_id) - if not user or user.deleted_at is not None: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User is not found" - ) - return user diff --git a/API/utils/database.py b/API/utils/database.py deleted file mode 100644 index 33ba21c..0000000 --- a/API/utils/database.py +++ /dev/null @@ -1,24 +0,0 @@ -from os import getenv -from typing import AsyncGenerator -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker,AsyncEngine - -engine: AsyncEngine = create_async_engine( - f"postgresql+asyncpg://{getenv("DB_USER")}:{getenv("DB_PASSWORD")}@db:5432/{getenv("DB_NAME")}", - pool_pre_ping=True -) - -AsyncSessionLocal = async_sessionmaker( - bind=engine, - class_=AsyncSession, - autocommit=False, - autoflush=False, - expire_on_commit=False -) - - -async def get_db() -> AsyncGenerator[AsyncSession, None]: - async with AsyncSessionLocal() as session: - try: - yield session - finally: - await session.close() \ No newline at end of file diff --git a/API/utils/xui.py b/API/utils/xui.py deleted file mode 100644 index 71d80a5..0000000 --- a/API/utils/xui.py +++ /dev/null @@ -1,176 +0,0 @@ -import json -import uuid -import random -from urllib.parse import quote -from httpx import AsyncClient -from fastapi import HTTPException - -class Configs: - def __init__(self, inbound_id: int, host: str, client_email: str, display_name: str): - self.inbound_id = inbound_id - self.host = host - self.client_email = client_email - self.display_name = display_name - - def legacy_payload(self, data: dict,) -> dict: - inbounds = data.get("obj") - inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None) - if not inbound: - raise HTTPException( - status_code=404, - detail=f"Server not found" - ) - stream_settings = json.loads(inbound.get("streamSettings")) - reality = stream_settings.get("realitySettings") - short_ids = reality.get("shortIds") - suid = random.choice(short_ids) - client_uuid = str(uuid.uuid4()) - return { - "id": self.inbound_id, - "settings": json.dumps({ - "clients": [{ - "id": client_uuid, - "flow": "xtls-rprx-vision", - "email": self.client_email, - "limitIp": 0, - "totalGB": 0, - "expiryTime": 0, - "enable": True, - "tgId": "", - "subId": suid, - "comment": "", - "reset": 0 - }] - }) - } - - def legacy_config(self, data: dict) -> str: - inbounds = data.get("obj") - inbound = next((i for i in inbounds if i.get("id") == self.inbound_id), None) - if not inbound: - raise HTTPException( - status_code=500, - detail="Server error" - ) - settings = json.loads(inbound.get("settings")) - clients = settings.get("clients") - current_client = next((i for i in clients if i.get("email") == self.client_email), None) - if not current_client: - raise HTTPException( - status_code=500, - detail="Server error" - ) - client_uuid = current_client.get("id") - sub_id = current_client.get("subId") - stream_settings = json.loads(inbound.get("streamSettings")) - reality = stream_settings.get("realitySettings") - server_names = reality.get("serverNames") - setting = reality.get("settings") - pbk = setting.get("publicKey") - fp = setting.get("fingerprint") - spx = quote(setting.get("spiderX"), safe='') - sni = random.choice(server_names) - port = inbound.get("port") - return ( - f"vless://{client_uuid}@{self.host}:{port}?" - f"security=reality&" - f"pbk={pbk}&" - f"fp={fp}&" - f"sni={sni}&" - f"sid={sub_id}&" - f"spx={spx}&" - f"flow=xtls-rprx-vision#" - f"{self.display_name}" - ) - -class XUIClient: - def __init__(self, host: str, base_url: str, username: str, password: str, inbound_id: int, version: str): - self.host = host - self.base_url = base_url.rstrip("/") - self.username = username - self.password = password - self.inbound_id = inbound_id - self.session = AsyncClient(timeout=20.0) - self.logged_in = False - self.version = version - - @classmethod - async def from_server(cls, server): - base_url = f"https://{server.host}:{server.port}" - return cls(server.host, base_url, server.user, server.password, server.inbound_id, server.version) - - async def _login(self): - if self.logged_in: - return - try: - resp = await self.session.post( - f"{self.base_url}/login", - json={"username": self.username, "password": self.password} - ) - resp.raise_for_status() - self.logged_in = True - except: - raise HTTPException( - status_code=500, - detail="Server error" - ) - - async def add_client(self, client_email: str, display_name: str) -> str: - configs = Configs(self.inbound_id, self.host, client_email, display_name) - await self._login() - resp = await self.session.post(f"{self.base_url}/panel/inbound/list") - resp.raise_for_status() - if self.version == "legacy": - resp = await self.session.post(f"{self.base_url}/panel/inbound/addClient", json=configs.legacy_payload(resp.json())) - #New configs here - else: - raise HTTPException( - status_code=500, - detail="Server error" - ) - resp.raise_for_status() - resp = await self.session.post(f"{self.base_url}/panel/inbound/list") - resp.raise_for_status() - if self.version == "legacy": - return configs.legacy_config(resp.json()) - #New configs here - else: - raise HTTPException( - status_code=500, - detail="Server error" - ) - - async def get_client_traffic(self, client_email: str) -> int: - await self._login() - try: - resp = await self.session.post(f"{self.base_url}/panel/inbound/list") - resp.raise_for_status() - data = resp.json() - inbounds = data.get("obj") - for inbound in inbounds: - client_stats = inbound.get("clientStats") - for stat in client_stats: - if stat.get("email") == client_email: - up = stat.get("up") - down = stat.get("down") - return up + down - return 0 - except: - return 0 - - async def delete_client(self, client_email: str): - await self._login() - try: - resp = await self.session.post( - f"{self.base_url}/panel/inbound/{self.inbound_id}/delClientByEmail/{client_email}" - ) - resp.raise_for_status() - return - except: - HTTPException( - status_code=500, - detail="Server error" - ) - - async def close(self): - await self.session.aclose() diff --git a/Frontend/Dockerfile b/Frontend/Dockerfile deleted file mode 100644 index 12f1639..0000000 --- a/Frontend/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM nginx:stable-alpine -COPY Web/ /usr/share/nginx/html/ -COPY Nginx/spectralvpn.ru.nginx /etc/nginx/conf.d/spectralvpn.ru.conf -COPY Nginx/spectralvpn.ru_http.nginx /etc/nginx/conf.d/spectralvpn.ru_http.conf -COPY Nginx/spectralvpn_api.nginx /etc/nginx/conf.d/spectralvpn_api.conf -EXPOSE 80 443 -CMD ["nginx", "-g", "daemon off;"] diff --git a/Frontend/JS/control-panel.js b/Frontend/JS/control-panel.js new file mode 100644 index 0000000..92f382d --- /dev/null +++ b/Frontend/JS/control-panel.js @@ -0,0 +1,225 @@ +const API_BASE = "https://spectralvpn.ru:8500"; + +const getCookie = (name) => { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? decodeURIComponent(match[2]) : null; +}; + +const deleteCookie = (name) => { + document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; SameSite=Strict`; +}; + +const sha256 = async (str) => { + const buf = new TextEncoder().encode(str); + const hash = await crypto.subtle.digest("SHA-256", buf); + return Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, "0")).join(""); +}; + +const showNotification = (text, color = "cyan") => { + const notification = document.createElement("div"); + notification.textContent = text; + notification.style.cssText = ` + position: fixed; top: 20px; right: 20px; padding: 15px 25px; border-radius: 12px; + z-index: 9999; font-weight: 600; background: ${color === "cyan" ? "#00ffff22" : "#ff444422"}; + color: ${color}; border: 1px solid ${color === "cyan" ? "cyan" : "#f66"}; + `; + document.body.appendChild(notification); + setTimeout(() => notification.remove(), 3000); +}; + +let currentEmail = null; +let currentHash = null; + +const modal = document.getElementById("loginModal"); +const showModal = () => modal.classList.add("active"); +const hideModal = () => modal.classList.remove("active"); + +const showLoginError = (msg) => { + document.getElementById("loginError").textContent = msg; +}; + +const renderUrls = (urls) => { + const container = document.getElementById("urlsList"); + container.innerHTML = ""; + + if (urls.length === 0) { + container.innerHTML = "

У вас пока нет конфигураций

"; + return; + } + + urls.forEach(name => { + const card = document.createElement("div"); + card.className = "url-card"; + + card.innerHTML = ` +
+
${name}
+
+
+ + +
+ `; + + card.querySelector(".btn-copy").onclick = async () => { + try { + const url = `${API_BASE}/get_url?email=${encodeURIComponent(currentEmail)}&password=${encodeURIComponent(currentHash)}&urls_name=${encodeURIComponent(name)}`; + const res = await fetch(url); + const data = await res.json(); + + if (res.ok && data.url) { + await navigator.clipboard.writeText(data.url); + showNotification(`Конфиг "${name}" скопирован!`); + } else { + showNotification("Ошибка получения конфига", "red"); + } + } catch (e) { + showNotification("Нет связи с сервером", "red"); + } + }; + + card.querySelector(".btn-delete").onclick = async () => { + if (!confirm(`Удалить конфиг "${name}"?`)) return; + + try { + const url = `${API_BASE}/del_url?email=${encodeURIComponent(currentEmail)}&password=${encodeURIComponent(currentHash)}&urls_name=${encodeURIComponent(name)}`; + const res = await fetch(url, { method: "DELETE" }); + + if (res.ok) { + card.remove(); + showNotification(`Конфиг "${name}" удалён`, "red"); + } else { + showNotification("Не удалось удалить", "red"); + } + } catch (e) { + showNotification("Ошибка сети", "red"); + } + }; + + container.appendChild(card); + }); +}; + +const tryAutoLogin = async () => { + currentEmail = getCookie("user_email"); + currentHash = getCookie("user_hash"); + + if (!currentEmail || !currentHash) { + showModal(); + return; + } + + try { + const res = await fetch(`${API_BASE}/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email: currentEmail, password: currentHash }) + }); + + if (res.ok) { + const data = await res.json(); + document.getElementById("userEmail").textContent = currentEmail; + document.getElementById("mainContent").classList.remove("hidden"); + renderUrls(data.urls || []); + } else { + deleteCookie("user_email"); + deleteCookie("user_hash"); + showModal(); + } + } catch (e) { + showModal(); + } +}; + +document.getElementById("loginSubmit").onclick = async () => { + const email = document.getElementById("loginEmail").value.trim(); + const pass = document.getElementById("loginPassword").value; + + if (!email || !pass) { + showLoginError("Заполните все поля"); + return; + } + + const hash = await sha256(pass); + + try { + const res = await fetch(`${API_BASE}/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password: hash }) + }); + + const data = await res.json(); + + if (res.ok) { + currentEmail = email; + currentHash = hash; + document.cookie = `user_email=${encodeURIComponent(email)}; path=/; max-age=2592000; Secure; SameSite=Strict`; + document.cookie = `user_hash=${hash}; path=/; max-age=2592000; Secure; SameSite=Strict`; + + document.getElementById("userEmail").textContent = email; + hideModal(); + document.getElementById("mainContent").classList.remove("hidden"); + renderUrls(data.urls || []); + } else { + showLoginError("Неверный email или пароль"); + } + } catch (e) { + showLoginError("Ошибка подключения"); + } +}; + +document.getElementById("addUrlBtn").onclick = async () => { + const name = prompt("Введите название конфигурации:"); + if (!name?.trim()) return; + + try { + const res = await fetch(`${API_BASE}/add_url`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + email: currentEmail, + password: currentHash, + urls_name: name.trim() + }) + }); + + const data = await res.json(); + + if (res.ok) { + renderUrls(data.urls); + showNotification(`Конфиг "${name}" создан! Скопируйте через кнопку.`); + } else { + showNotification("Ошибка: " + (data.detail || "неизвестно"), "red"); + } + } catch (e) { + showNotification("Нет связи с сервером", "red"); + } +}; + +document.getElementById("logoutBtn").onclick = () => { + deleteCookie("user_email"); + deleteCookie("user_hash"); + location.reload(); +}; + +document.getElementById("closeModal").onclick = hideModal; + +tryAutoLogin(); + +document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + + document.querySelectorAll('.instructions').forEach(el => el.classList.add('hidden')); + + const platform = btn.dataset.platform; + const target = document.getElementById(`${platform}-instructions`); + if (target) { + target.classList.remove('hidden'); + } + }); +}); + +document.addEventListener('DOMContentLoaded', () => {}); \ No newline at end of file diff --git a/Frontend/Web/JS/register.js b/Frontend/JS/register.js similarity index 51% rename from Frontend/Web/JS/register.js rename to Frontend/JS/register.js index 017110d..acb49ff 100644 --- a/Frontend/Web/JS/register.js +++ b/Frontend/JS/register.js @@ -4,20 +4,26 @@ document.addEventListener("DOMContentLoaded", () => { const setError = (fieldId, message) => { const errorEl = document.getElementById(fieldId); - if (errorEl) { - errorEl.textContent = message; - errorEl.classList.add("active"); - } - const input = document.getElementById(fieldId.replace("Error", "")); - if (input) input.classList.add("invalid"); + const inputEl = errorEl.previousElementSibling; + errorEl.textContent = message; + errorEl.classList.add("active"); + if (inputEl) inputEl.classList.add("invalid"); }; const clearErrors = () => { - document.querySelectorAll(".error").forEach(el => { + form.querySelectorAll(".error").forEach(el => { el.textContent = ""; el.classList.remove("active"); }); - document.querySelectorAll(".input").forEach(el => el.classList.remove("invalid")); + form.querySelectorAll(".invalid").forEach(el => el.classList.remove("invalid")); + document.querySelector(".checkbox-container")?.classList.remove("invalid"); + }; + + const sha256 = async (message) => { + const msgBuffer = new TextEncoder().encode(message); + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); }; form.addEventListener("submit", async (e) => { @@ -27,49 +33,59 @@ document.addEventListener("DOMContentLoaded", () => { const email = document.getElementById("email").value.trim(); const password = document.getElementById("password").value; const passwordReply = document.getElementById("password_reply").value; - const promo_code = document.getElementById("promo_code") ? document.getElementById("promo_code").value.trim() : ""; + const terms = document.getElementById("terms").checked; + + let hasError = false; if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setError("emailError", "Введите корректный email"); - return; + hasError = true; } - if (!password || password.length < 6) { - setError("passwordError", "Пароль минимум 6 символов"); - return; + if (!password || password.length < 6 || !/(?=.*[A-Za-z])(?=.*\d)/.test(password)) { + setError("passwordError", "Минимум 6 символов, буквы + цифры"); + hasError = true; } if (password !== passwordReply) { setError("passwordReplyError", "Пароли не совпадают"); - return; + hasError = true; } + if (!terms) { + setError("termsError", "Принять условия обязательно"); + document.querySelector(".checkbox-container").classList.add("invalid"); + hasError = true; + } + + if (hasError) return; submitButton.disabled = true; submitButton.textContent = "Создаём аккаунт..."; try { - const response = await fetch("https://spectralvpn.ru:8000/user/signup", { + const passwordHash = await sha256(password); + + const response = await fetch("https://spectralvpn.ru:8500/registration", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + }, body: JSON.stringify({ email: email, - password: password, - promo_code: promo_code + password: passwordHash }) }); const result = await response.json(); if (response.ok) { - // Сохраняем токен - localStorage.setItem("access_token", result.access_token); + Cookies.set("user_email", email, { expires: 30, sameSite: "strict" }); + Cookies.set("user_hash", passwordHash, { expires: 30, sameSite: "strict" }); window.location.href = "control-panel.html"; } else { - if (result.detail?.includes("Promo-code")) { - setError("promoError", "Неверный промокод"); - } else if (result.detail?.includes("Email") || result.detail?.includes("busy")) { - setError("emailError", "Этот email уже используется"); + if (result.detail === "Email is busy.") { + setError("emailError", "Этот email уже зарегистрирован"); } else { - setError("emailError", result.detail || "Ошибка регистрации"); + setError("emailError", result.detail || "Ошибка сервера"); } } } catch (err) { diff --git a/Frontend/Nginx/spectralvpn_api.nginx b/Frontend/Nginx/spectralvpn_api.nginx deleted file mode 100644 index 9f7c97f..0000000 --- a/Frontend/Nginx/spectralvpn_api.nginx +++ /dev/null @@ -1,34 +0,0 @@ -server { - listen 8000 ssl; - listen [::]:8000 ssl; - server_name spectralvpn.ru; - - ssl_certificate /etc/letsencrypt/live/spectralvpn.ru/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/spectralvpn.ru/privkey.pem; - - location / { - if ($request_method = 'OPTIONS') { - add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru' always; - add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS' always; - add_header 'Access-Control-Allow-Headers' 'X-API-KEY, Content-Type, Authorization' always; - add_header 'Access-Control-Max-Age' 86400 always; - add_header 'Access-Control-Allow-Credentials' 'true' always; - return 204; - } - - add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru' always; - add_header 'Access-Control-Allow-Credentials' 'true' always; - add_header 'Access-Control-Expose-Headers' '*' always; # если нужно читать заголовки ответа - - proxy_pass http://api:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-Forwarded-Host $host; - proxy_set_header X-Forwarded-Port $server_port; - - proxy_hide_header Access-Control-Allow-Origin; - proxy_hide_header Access-Control-Allow-Credentials; - } -} \ No newline at end of file diff --git a/Frontend/Web/Styles/control-panel.css b/Frontend/Styles/control-panel.css similarity index 53% rename from Frontend/Web/Styles/control-panel.css rename to Frontend/Styles/control-panel.css index 829b5a7..70d2f66 100644 --- a/Frontend/Web/Styles/control-panel.css +++ b/Frontend/Styles/control-panel.css @@ -1,294 +1,261 @@ -:root { +:root{ --bg: #000; --card: #111; --text: #e0e0e0; - --accent: #00ffff; + --accent: cyan; --border: #333; - --danger: #ff4444; } -* { margin: 0; padding: 0; box-sizing: border-box; } -body { - background: var(--bg); - color: var(--text); - font-family: 'Inter', sans-serif; - min-height: 100vh; - line-height: 1.5; -} +*{margin: 0; padding: 0; box-sizing: border-box;} +body{background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh;} -.container { max-width: 820px; margin: 0 auto; padding: 20px; } +.container{max-width: 800px; margin: 0 auto; padding: 20px;} -header { +header{ display: flex; justify-content: space-between; align-items: center; padding: 20px 0; - border-bottom: 1px solid var(--border); + border-bottom: 1px solid #333; margin-bottom: 40px; flex-wrap: wrap; - gap: 15px; } -header h2 { font-size: 28px; } -header h2 b { color: var(--accent); } +header h2 {font-size: 24pt;} +header h2 b {color: var(--accent);} -.user-info { +.user-info{ display: flex; align-items: center; gap: 15px; - font-size: 15px; + font-size: 14pt; } -#logoutBtn { +#logoutBtn{ background: transparent; border: 1px solid var(--accent); color: var(--accent); - padding: 8px 18px; + padding: 8px 16px; border-radius: 8px; cursor: pointer; transition: 0.3s; } -#logoutBtn:hover { +#logoutBtn:hover{ background: var(--accent); color: #000; } -h1 { - font-size: 26px; - margin-bottom: 25px; +h1{ + font-size: 28pt; + margin-bottom: 30px; text-align: center; } -.urls-list { +.urls-list{ display: grid; - gap: 16px; + gap: 15px; margin-bottom: 30px; } -.url-card { +.url-card{ background: var(--card); - border: 1px solid var(--border); - border-radius: 14px; + border: 1px solid #333; + border-radius: 12px; padding: 20px; display: flex; justify-content: space-between; align-items: center; - transition: all 0.3s ease; + transition: 0.3s; } -.url-card:hover { +.url-card:hover{ border-color: var(--accent); - box-shadow: 0 0 20px rgba(0, 255, 255, 0.15); + box-shadow: 0 0 15px rgba(0, 255, 255, 0.2); } -.url-info { - flex: 1; -} - -.url-name { +.url-name{ font-weight: 600; color: var(--accent); - font-size: 17px; } -.traffic { - font-size: 13.5px; - color: #888; - margin-top: 4px; -} - -.url-actions { +.url-actions{ display: flex; gap: 10px; } -.btn { - padding: 9px 18px; +.btn-copy, .btn-delete{ + padding: 8px 16px; border-radius: 8px; cursor: pointer; font-weight: 600; transition: 0.3s; - border: none; } -.btn-copy { +.btn-copy{ background: transparent; border: 1px solid var(--accent); color: var(--accent); } -.btn-copy:hover { +.btn-copy:hover{ background: var(--accent); color: #000; } -.btn-delete { - background: #2a0a0a; +.btn-delete{ + background: #330000; border: 1px solid #800; - color: #ff8888; + color: #f88; } -.btn-delete:hover { +.btn-delete:hover{ background: #800; color: white; } -.add-btn { +.add-btn{ width: 100%; - padding: 18px; + padding: 16px; background: transparent; border: 2px dashed var(--accent); color: var(--accent); - font-size: 17px; - font-weight: 600; + font-size: 16pt; border-radius: 12px; cursor: pointer; - transition: 0.3s; + transition:hover{ + background: rgba(0, 255, 255, 0.1); + } } -.add-btn:hover { - background: rgba(0, 255, 255, 0.08); -} - -.danger-zone { - margin-top: 60px; - text-align: center; -} - -.delete-account-btn { - background: #3a0a0a; - color: var(--danger); - border: 1px solid #a00; - padding: 12px 28px; - border-radius: 10px; - cursor: pointer; - font-size: 15px; -} - -.delete-account-btn:hover { - background: #600; - color: white; -} - -/* Модальное окно */ -.modal { +.modal{ display: none; position: fixed; - inset: 0; - background: rgba(0,0,0,0.85); + top: 0; left: 0; width: 100%; height: 100%; + background: rgba(0,0,0,0.8); justify-content: center; align-items: center; - z-index: 2000; + z-index: 1000; } -.modal.active { display: flex; } +.modal.active{display: flex;} -.modal-content { +.modal-content{ background: #111; - padding: 32px; + padding: 30px; border-radius: 16px; width: 90%; - max-width: 420px; + max-width: 400px; border: 1px solid var(--accent); -} - -.modal-content h2 { margin-bottom: 20px; text-align: center; } - -.modal-content input { - width: 100%; - padding: 14px; - margin-bottom: 15px; - background: #222; - border: 1px solid #555; - border-radius: 10px; - color: white; - font-size: 16px; -} - -.error { - color: #ff6b6b; - min-height: 22px; - font-size: 14px; text-align: center; } -.buttons { +.modal-content input{ + width: 100%; + padding: 14px; + margin: 10px 0; + background: #222; + border: 1px solid #444; + border-radius: 10px; + color: white; +} + +.modal-content .error{ + color: #ff6b6b; + margin: 10px 0; + min-height: 20px; +} + +.buttons{ display: flex; - gap: 12px; + gap: 10px; margin-top: 20px; } -.buttons button { +.buttons button{ flex: 1; - padding: 14px; + padding: 12px; border-radius: 10px; cursor: pointer; - font-weight: 600; } -#addSubmit { +#loginSubmit{ background: var(--accent); - color: #000; + color: black; border: none; } -#addCancel { +#closeModal{ background: transparent; border: 1px solid #666; - color: #ccc; + color: #aaa; } -/* Табы и инструкции (оставил твои стили, только немного подчистил) */ -.platform-tabs { +.how-to-connect{ + margin-bottom: 60px; +} + +.platform-tabs{ display: flex; justify-content: center; gap: 12px; - margin-bottom: 30px; + margin-bottom: 32px; flex-wrap: wrap; } -.tab-btn { +.tab-btn{ background: #1a1a1a; border: 1px solid #444; color: #aaa; - padding: 11px 22px; + padding: 12px 24px; border-radius: 50px; + font-size: 15px; + font-weight: 500; cursor: pointer; - transition: all 0.25s; + transition: all 0.22s ease; } -.tab-btn.active, -.tab-btn:hover { +.tab-btn:hover{ + border-color: #666; + color: #ddd; + background: #222; +} + +.tab-btn.active{ background: var(--accent); color: #000; border-color: var(--accent); + box-shadow: 0 0 20px rgba(0, 255, 255, 0.25); + font-weight: 600; } -.instructions { +.instructions{ background: var(--card); border: 1px solid #333; border-radius: 16px; - padding: 26px 30px; + padding: 28px 32px; + transition: all 0.3s ease; } -.instructions ol { +.instructions ol{ counter-reset: step; list-style: none; + font-size: 15.5px; + line-height: 1.65; } -.instructions li { +.instructions li{ position: relative; - padding-left: 42px; - margin-bottom: 16px; + padding-left: 38px; + margin-bottom: 18px; } -.instructions li::before { +.instructions li::before{ content: counter(step); counter-increment: step; position: absolute; left: 0; - top: 0; + top: 2px; width: 28px; height: 28px; background: #222; @@ -298,7 +265,37 @@ h1 { display: flex; align-items: center; justify-content: center; - font-weight: 700; + font-size: 14px; + font-weight: 600; } -.hidden { display: none !important; } \ No newline at end of file +.instructions a{ + color: var(--accent); + text-decoration: none; + border-bottom: 1px solid rgba(0,255,255,0.3); + transition: all 0.2s; +} + +.instructions a:hover{ + border-bottom-color: var(--accent); + color: #00ffff; +} + +.instructions kbd{ + background: #1e1e1e; + border: 1px solid #444; + border-radius: 5px; + padding: 3px 7px; + font-family: monospace; + color: #aaa; +} + +.instructions ul{ + margin: 12px 0 12px 24px; + list-style: disc; + color: #bbb; +} + +.hidden{ + display: none !important; +} \ No newline at end of file diff --git a/Frontend/Web/Styles/index.css b/Frontend/Styles/index.css similarity index 100% rename from Frontend/Web/Styles/index.css rename to Frontend/Styles/index.css diff --git a/Frontend/Web/Styles/offer.css b/Frontend/Styles/offer.css similarity index 100% rename from Frontend/Web/Styles/offer.css rename to Frontend/Styles/offer.css diff --git a/Frontend/Web/Styles/register.css b/Frontend/Styles/register.css similarity index 100% rename from Frontend/Web/Styles/register.css rename to Frontend/Styles/register.css diff --git a/Frontend/Web/JS/control-panel.js b/Frontend/Web/JS/control-panel.js deleted file mode 100644 index a8da9d5..0000000 --- a/Frontend/Web/JS/control-panel.js +++ /dev/null @@ -1,251 +0,0 @@ -const API_BASE = "https://spectralvpn.ru:8000"; - -const getToken = () => localStorage.getItem("access_token"); - -const showNotification = (message, type = "success") => { - const color = type === "success" ? "#00ffff" : "#ff5555"; - const notif = document.createElement("div"); - notif.style.cssText = ` - position: fixed; top: 20px; right: 20px; padding: 14px 24px; border-radius: 12px; - background: rgba(0,0,0,0.96); border: 1px solid ${color}; color: ${color}; - z-index: 10000; font-weight: 500; box-shadow: 0 4px 20px rgba(0,0,0,0.6); - `; - notif.textContent = message; - document.body.appendChild(notif); - setTimeout(() => notif.remove(), 4500); -}; - -async function apiRequest(endpoint, options = {}) { - const token = getToken(); - if (!token) { - showLoginModal(); - return null; - } - - const res = await fetch(`${API_BASE}${endpoint}`, { - ...options, - headers: { - "X-API-KEY": token, - "Content-Type": "application/json", - ...options.headers - } - }); - - if (res.status === 401) { - localStorage.removeItem("access_token"); - showNotification("Сессия истекла. Войдите заново.", "error"); - showLoginModal(); - return null; - } - - return res; -} - -function showLoginModal() { - document.getElementById("loginModal").classList.add("active"); - document.getElementById("mainContent").classList.add("hidden"); - document.getElementById("userInfo").style.display = "none"; -} - -function hideLoginModal() { - document.getElementById("loginModal").classList.remove("active"); - document.getElementById("mainContent").classList.remove("hidden"); - document.getElementById("userInfo").style.display = "flex"; -} - -async function loadPanel() { - const token = getToken(); - if (!token) { - showLoginModal(); - return; - } - - const res = await apiRequest("/config/get_info", { method: "GET" }); - if (!res) return; - - if (!res.ok) { - showNotification("Ошибка загрузки данных", "error"); - return; - } - - const data = await res.json(); - - document.getElementById("userEmail").textContent = data.email || "Аккаунт активен"; - hideLoginModal(); - renderConfigs(data.configs || []); -} - -function renderConfigs(configs) { - const container = document.getElementById("urlsList"); - container.innerHTML = ""; - - if (configs.length === 0) { - container.innerHTML = `

- У вас пока нет конфигураций.
Нажмите кнопку ниже, чтобы создать первую. -

`; - return; - } - - configs.forEach(cfg => { - const trafficGB = (cfg.bytes_used / (1024 ** 3)).toFixed(2); - - const card = document.createElement("div"); - card.className = "url-card"; - card.innerHTML = ` -
-
${cfg.name}
-
Использовано: ${trafficGB} ГБ
-
-
- - -
- `; - - card.querySelector(".btn-copy").addEventListener("click", async () => { - try { - await navigator.clipboard.writeText(cfg.config); - showNotification(`Конфиг "${cfg.name}" скопирован`); - } catch (e) { - showNotification("Не удалось скопировать", "error"); - } - }); - - card.querySelector(".btn-delete").addEventListener("click", async () => { - if (!confirm(`Удалить конфигурацию "${cfg.name}"?`)) return; - - const res = await apiRequest("/config/delete", { - method: "DELETE", - body: JSON.stringify({ name: cfg.name }) - }); - - if (res && res.ok) { - showNotification(`Конфиг "${cfg.name}" удалён`); - loadPanel(); - } else { - showNotification("Не удалось удалить конфиг", "error"); - } - }); - - container.appendChild(card); - }); -} - -document.getElementById("loginSubmit").addEventListener("click", async () => { - const email = document.getElementById("loginEmail").value.trim(); - const password = document.getElementById("loginPassword").value; - const errorEl = document.getElementById("loginError"); - - errorEl.textContent = ""; - - if (!email || !password) { - errorEl.textContent = "Введите email и пароль"; - return; - } - - try { - const res = await fetch(`${API_BASE}/user/login`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }) - }); - - const data = await res.json(); - - if (res.ok && data.access_token) { - localStorage.setItem("access_token", data.access_token); - showNotification("Вход выполнен успешно!"); - document.getElementById("loginEmail").value = ""; - document.getElementById("loginPassword").value = ""; - loadPanel(); - } else { - errorEl.textContent = data.detail || "Неверный email или пароль"; - } - } catch (err) { - console.error(err); - errorEl.textContent = "Нет связи с сервером. Попробуйте позже."; - } -}); - -document.getElementById("addConfigBtn").addEventListener("click", () => { - document.getElementById("addModal").classList.add("active"); - document.getElementById("configName").focus(); -}); - -document.getElementById("addCancel").addEventListener("click", () => { - document.getElementById("addModal").classList.remove("active"); - document.getElementById("addError").textContent = ""; -}); - -document.getElementById("addSubmit").addEventListener("click", async () => { - const name = document.getElementById("configName").value.trim(); - const errorEl = document.getElementById("addError"); - - if (!name) { - errorEl.textContent = "Введите название конфигурации"; - return; - } - - const res = await apiRequest("/config/add", { - method: "POST", - body: JSON.stringify({ name }) - }); - - if (res && res.ok) { - showNotification(`Конфигурация "${name}" создана!`); - document.getElementById("addModal").classList.remove("active"); - document.getElementById("configName").value = ""; - loadPanel(); - } else { - const errData = await res.json().catch(() => ({})); - errorEl.textContent = errData.detail || "Ошибка при создании"; - } -}); - -document.getElementById("logoutBtn").addEventListener("click", async () => { - if (!confirm("Выйти из аккаунта?")) return; - - const token = getToken(); - if (token) { - await fetch(`${API_BASE}/user/logout`, { - method: "POST", - headers: { "X-API-KEY": token, "Content-Type": "application/json" }, - body: JSON.stringify({ token_to_revoke: token }) - }).catch(() => {}); - } - - localStorage.removeItem("access_token"); - showLoginModal(); -}); - -document.getElementById("deleteAccountBtn").addEventListener("click", async () => { - if (!confirm("Вы уверены? Это действие необратимо!")) return; - - const res = await apiRequest("/user/delete", { method: "DELETE" }); - - if (res && res.ok) { - localStorage.removeItem("access_token"); - alert("Аккаунт успешно удалён."); - showLoginModal(); - } else { - showNotification("Не удалось удалить аккаунт", "error"); - } -}); - -document.querySelectorAll(".tab-btn").forEach(btn => { - btn.addEventListener("click", () => { - document.querySelectorAll(".tab-btn").forEach(b => b.classList.remove("active")); - btn.classList.add("active"); - - document.querySelectorAll(".instructions").forEach(instr => instr.classList.add("hidden")); - document.getElementById(btn.dataset.platform + "-instructions").classList.remove("hidden"); - }); -}); - -document.getElementById("addModal").addEventListener("click", (e) => { - if (e.target === document.getElementById("addModal")) { - document.getElementById("addModal").classList.remove("active"); - } -}); - -document.addEventListener("DOMContentLoaded", loadPanel); \ No newline at end of file diff --git a/Frontend/Web/control-panel.html b/Frontend/Web/control-panel.html deleted file mode 100644 index 4df7588..0000000 --- a/Frontend/Web/control-panel.html +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - Личный кабинет — SpectralVPN - - - - -
-
-

SpectralVPN

- -
- -
-
-

Как подключиться

-
- - - -
- -
-
    -
  1. Создайте конфигурацию ниже и скопируйте ссылку.
  2. -
  3. Установите v2rayNG.
  4. -
  5. Нажмите + → Импорт из буфера обмена.
  6. -
  7. Включайте соединение большой кнопкой.
  8. -
-
- - - - -
- -

Ваши конфигурации

-
- - -
- -
-
- - - - -
- - - - \ No newline at end of file diff --git a/Frontend/control-panel.html b/Frontend/control-panel.html new file mode 100644 index 0000000..96f23f4 --- /dev/null +++ b/Frontend/control-panel.html @@ -0,0 +1,88 @@ + + + + + + Личный кабинет — SpectralVPN + + + + +
+
+

SpectralVPN

+ +
+ +
+
+

Как подключиться

+ +
+ + + +
+ +
+
    +
  1. Создайте и скопируйте свою конфигурацию в личном кабинете.
  2. +
  3. Скачайте и установите приложение v2rayNG.
  4. +
  5. Откройте приложение.
  6. +
  7. Нажмите + в правом верхнем углу → Импорт из буфера обмена.
  8. +
  9. Готово! Включайте/выключайте соединение большой кнопкой внизу справа.
  10. +
+
+ + + + +
+

Ваши конфигурации

+
+ +
+ + +
+ + + + \ No newline at end of file diff --git a/Frontend/Web/index.html b/Frontend/index.html similarity index 100% rename from Frontend/Web/index.html rename to Frontend/index.html diff --git a/Frontend/Web/offer.html b/Frontend/offer.html similarity index 100% rename from Frontend/Web/offer.html rename to Frontend/offer.html diff --git a/Frontend/Web/privacy.html b/Frontend/privacy.html similarity index 100% rename from Frontend/Web/privacy.html rename to Frontend/privacy.html diff --git a/Frontend/Web/register.html b/Frontend/register.html similarity index 90% rename from Frontend/Web/register.html rename to Frontend/register.html index 7d3dc13..0a350af 100644 --- a/Frontend/Web/register.html +++ b/Frontend/register.html @@ -21,9 +21,6 @@
- - -
diff --git a/README.md b/README.md deleted file mode 100644 index 6ee2700..0000000 --- a/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# SpectralVPN - -> Быстрый, лёгкий и современный VPN-сервер с удобным веб-интерфейсом - -![Python](https://img.shields.io/badge/Python-3776AB?logo=python&logoColor=white) ![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?logo=javascript&logoColor=black) ![Docker](https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=white) ![License](https://img.shields.io/github/license/lev4ikysss/SpectralVPN) - -SpectralVPN — это открытый VPN-решение, состоящее из бэкенда (API) и современного фронтенда. Проект предназначен для быстрого развёртывания собственного VPN-сервера с удобным управлением через веб-панель. - -## ✨ Возможности - -- Современный REST API на Python - Красивый и responsive веб-интерфейс (Frontend) - Поддержка Docker и docker-compose (быстрое развёртывание) - Управление пользователями, конфигурациями и сессиями - Лёгкая расширяемость - -## 🛠 Технологический стек - -- \*\*Backend\*\*: Python + FastAPI (предположительно) - \*\*Frontend\*\*: HTML, CSS, JavaScript (возможно React/Vue или vanilla с современным дизайном) - \*\*Контейнеризация\*\*: Docker + docker-compose - \*\*База данных\*\*: (укажи, если используешь SQLite/PostgreSQL/Redis) diff --git a/configs/config/params.conf b/configs/config/params.conf new file mode 100644 index 0000000..422f9ef --- /dev/null +++ b/configs/config/params.conf @@ -0,0 +1,7 @@ +[api] +path = spectralvpn.db +host = +username = +password = +inbaund_id = 0 +inbaund_url = spectralvpn.ru:442 \ No newline at end of file diff --git a/Frontend/Nginx/spectralvpn.ru.nginx b/configs/nginx/spectralvpn.ru.nginx similarity index 91% rename from Frontend/Nginx/spectralvpn.ru.nginx rename to configs/nginx/spectralvpn.ru.nginx index 75259b3..69a618b 100644 --- a/Frontend/Nginx/spectralvpn.ru.nginx +++ b/configs/nginx/spectralvpn.ru.nginx @@ -10,7 +10,7 @@ server { gzip_types text/css text/javascript text/plain application/javascript application/json; gzip_min_length 1000; - root /usr/share/nginx/html/; + root /var/www/html/spectralvpn.ru; index index.html; location / { diff --git a/Frontend/Nginx/spectralvpn.ru_http.nginx b/configs/nginx/spectralvpn.ru_http.nginx similarity index 98% rename from Frontend/Nginx/spectralvpn.ru_http.nginx rename to configs/nginx/spectralvpn.ru_http.nginx index 4203451..304a955 100644 --- a/Frontend/Nginx/spectralvpn.ru_http.nginx +++ b/configs/nginx/spectralvpn.ru_http.nginx @@ -3,4 +3,4 @@ server { listen [::]:80; server_name spectralvpn.ru; return 301 https://$host$request_uri; -} +} \ No newline at end of file diff --git a/configs/nginx/spectralvpn_api.nginx b/configs/nginx/spectralvpn_api.nginx new file mode 100644 index 0000000..ecfb3d1 --- /dev/null +++ b/configs/nginx/spectralvpn_api.nginx @@ -0,0 +1,30 @@ +server { + listen 8500 ssl; + listen [::]:8500 ssl; + server_name vpn.spectralvpn.ru; + + ssl_certificate /etc/letsencrypt/live/vpn.spectralvpn.ru/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/vpn.spectralvpn.ru/privkey.pem; + + location / { + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' 'https://spectralvpn.ru'; + add_header 'Access-Control-Allow-Origin' 'https://vpn.spectralvpn.ru'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, DELETE, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'Content-Type'; + add_header 'Access-Control-Max-Age' 86400; + return 204; + } + + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + + add_header Access-Control-Allow-Origin "https://spectralvpn.ru" always; + add_header Access-Control-Allow-Origin "https://vpn.spectralvpn.ru" always; + } +} \ No newline at end of file diff --git a/configs/systemd/spectralvpn_api.service b/configs/systemd/spectralvpn_api.service new file mode 100644 index 0000000..eb515f6 --- /dev/null +++ b/configs/systemd/spectralvpn_api.service @@ -0,0 +1,16 @@ +[Unit] +Description=API SpectralVPN +After=network.target +Wants=network.target + +[Service] +User = root +WorkingDirectory=/etc/spectralvpn_api +Environment="PATH=/etc/spectralvpn_api/.venv/bin" +ExecStart=/etc/spectralvpn_api/.venv/bin/uvicorn main:app +Restart=always +RestartSec=3 +KillMode=control-group + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index c2c5836..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -services: - api: - build: ./API - container_name: spectralvpn-api - restart: unless-stopped - depends_on: - db: - condition: service_healthy - environment: - DB_NAME: ${DB_NAME} - DB_USER: ${DB_USER} - DB_PASSWORD: ${DB_PASSWORD} - extra_hosts: - - host.docker.internal:172.17.0.1 - ports: - - 8000:8000 - web: - build: ./Frontend - container_name: spectralvpn-web - restart: unless-stopped - depends_on: - - api - ports: - - 80:80 - - 443:443 - db: - image: postgres:latest - container_name: spectralvpn-db - restart: unless-stopped - environment: - POSTGRES_DB: ${DB_NAME} - POSTGRES_USER: ${DB_USER} - POSTGRES_PASSWORD: ${DB_PASSWORD} - healthcheck: - test: ["CMD", "pg_isready", "-U", "${DB_USER}", "-d", "${DB_NAME}"] - interval: 5s - timeout: 5s - retries: 10 - volumes: - - db-data:/var/lib/postgresql - #TODO Удалить проброс портов - ports: - - 5432:5432 - -volumes: - db-data: diff --git a/requirments.txt b/requirments.txt new file mode 100644 index 0000000..d3a29c4 --- /dev/null +++ b/requirments.txt @@ -0,0 +1,4 @@ +fastapi[all] +pandas +configparser +requests \ No newline at end of file