diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a29be8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Viktor K. (mrkrk) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/database/__init__.py b/database/__init__.py index c565446..4ff63c3 100644 --- a/database/__init__.py +++ b/database/__init__.py @@ -2,6 +2,8 @@ from models import settings from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.orm import DeclarativeBase +from .user import * + engine = create_async_engine(settings.database) sessions = async_sessionmaker(engine) diff --git a/database/user.py b/database/user.py new file mode 100644 index 0000000..62a7914 --- /dev/null +++ b/database/user.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import Mapped, mapped_column + +from database import Base + + +class User(Base): + __tablename__ = "users" + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column(unique=True) + password: Mapped[str] + salt: Mapped[str] diff --git a/docker-compose.yml b/docker-compose.yml index d5ec871..2497ff7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,8 @@ services: - DATABASE=postgresql+asyncpg://${DATABASE_USER}:${DATABASE_PASSWORD}@inputly-database:5432/${DATABASE_NAME} - SECRET=${SECRET} - PORT=${PORT} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - DISABLE_ADMIN=${DISABLE_ADMIN} ports: - ${PORT}:${PORT} depends_on: diff --git a/endpoints/__init__.py b/endpoints/__init__.py deleted file mode 100644 index af9233c..0000000 --- a/endpoints/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from fastapi import APIRouter - -router = APIRouter() diff --git a/main.py b/main.py index 1f4aaa4..9161668 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from fastapi import FastAPI import models import database -import endpoints +import routes logging.basicConfig(level=logging.INFO) @@ -19,6 +19,6 @@ async def lifespan(app: FastAPI): yield app = FastAPI(lifespan=lifespan) -app.include_router(endpoints.router) +app.include_router(routes.router) uvicorn.run(app, host="0.0.0.0", port=models.settings.port) diff --git a/models/__init__.py b/models/__init__.py index 7d7765a..030e7b4 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1 +1,10 @@ -from .settings import * +import pydantic + + +class BaseModel(pydantic.BaseModel): + class Config: + orm_mode = True + + +from .settings import settings +from .user import * diff --git a/models/settings.py b/models/settings.py index c0e818f..3c14b79 100644 --- a/models/settings.py +++ b/models/settings.py @@ -5,6 +5,8 @@ class Settings(BaseSettings): database: str secret: str port: int + admin_password: str + disable_admin: bool settings = Settings() diff --git a/models/user.py b/models/user.py new file mode 100644 index 0000000..ff202d3 --- /dev/null +++ b/models/user.py @@ -0,0 +1,19 @@ +from pydantic import Field + +from models import BaseModel + + +class User(BaseModel): + id: int + username: str + + +class Auth(BaseModel): + username: str + password: str + + +class Token(BaseModel): + id: int + username: str + token: str diff --git a/pyproject.toml b/pyproject.toml index f466f42..59d4c4c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "inputly-server" version = "0.1.0" description = "Simple forms service backend" authors = ["Viktor K. "] -license = "APGL-3.0-only" +license = "MIT" readme = "README.md" [tool.poetry.dependencies] diff --git a/routes/__init__.py b/routes/__init__.py new file mode 100644 index 0000000..89e1dda --- /dev/null +++ b/routes/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter + +from . import admin + +router = APIRouter() + +router.include_router(admin.router) diff --git a/routes/admin.py b/routes/admin.py new file mode 100644 index 0000000..08cdf94 --- /dev/null +++ b/routes/admin.py @@ -0,0 +1,52 @@ +import secrets +from typing import Annotated +import hashlib + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy import select + +import database +from models import settings, user + +router = APIRouter(prefix="/admin") + + +def hash_password(password: str, salt: str) -> str: + return hashlib.sha512((password + salt).encode('utf-8')).hexdigest() + + +def verify_admin(token: str): + if token != settings.admin_password: + raise HTTPException(401, "Unauthorized") + return True + + +Admin = Annotated[bool, Depends(verify_admin, use_cache=False)] + + +@router.post("/user") +async def create_user(auth: user.Auth, admin_token: Admin): + if len(auth.username.strip()) == 0: + raise HTTPException(400, "Username must not be empty") + if len(auth.password.strip()) == 0: + raise HTTPException(400, "Password must not be empty") + if settings.disable_admin: + raise HTTPException(403, "You are not admin") + + salt = secrets.token_hex(8) + + async with database.sessions.begin() as session: + stmt = select(database.User).where(database.User.username == auth.username) + db_user = session.execute(stmt).scalar_one_or_none() + if db_user is not None: + raise HTTPException(400, "User with this username already exists") + + new_user = database.User( + username=auth.username, + password=hash_password(auth.password, salt), + salt=salt, + ) + session.add(new_user) + await session.flush() + + return {'status': 'Success'} \ No newline at end of file diff --git a/routes/user.py b/routes/user.py new file mode 100644 index 0000000..5014484 --- /dev/null +++ b/routes/user.py @@ -0,0 +1,8 @@ +import hashlib + +from fastapi import APIRouter +from fastapi.security import OAuth2PasswordBearer + +import database + +router = APIRouter(prefix="/user")