Remove backend folder
This commit is contained in:
parent
b7f7a8964a
commit
5f63929477
30 changed files with 4 additions and 1434 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -48,3 +48,7 @@ supabase/.branches
|
|||
|
||||
# Podman
|
||||
.podman-compose
|
||||
|
||||
# AI
|
||||
.claude
|
||||
.codex
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
3.12
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
# Use a Python image with uv pre-installed
|
||||
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim
|
||||
|
||||
# Install the project into `/app`
|
||||
WORKDIR /app
|
||||
|
||||
# Enable bytecode compilation
|
||||
ENV UV_COMPILE_BYTECODE=1
|
||||
|
||||
# Copy from the cache instead of linking since it's a mounted volume
|
||||
ENV UV_LINK_MODE=copy
|
||||
|
||||
# Install system dependencies in one go
|
||||
# Use optimization to reduce installation time and image size
|
||||
# Update package lists and install dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
curl \
|
||||
npm && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Get Rust
|
||||
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
|
||||
|
||||
# Add .cargo/bin to PATH
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
|
||||
# Install the project's dependencies using the lockfile and settings
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --no-install-project --no-dev
|
||||
|
||||
# Then, add the rest of the project source code and install it
|
||||
# Installing separately from its dependencies allows optimal layer caching
|
||||
ADD . /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
# Place executables in the environment at the front of the path
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Reset the entrypoint, don't invoke `uv`
|
||||
ENTRYPOINT []
|
||||
|
||||
# Run the FastAPI application by default
|
||||
# Uses `fastapi dev` to enable hot-reloading when the `watch` sync occurs
|
||||
# Uses `--host 0.0.0.0` to allow access from outside the container
|
||||
CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "80", "app/main.py"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,31 +0,0 @@
|
|||
# Domain
|
||||
# This would be set to the production domain with an env var on deployment
|
||||
DOMAIN=localhost
|
||||
|
||||
# FRONTEND_HOST=http://localhost:5173
|
||||
|
||||
# Environment: local, staging, production
|
||||
ENVIRONMENT=local
|
||||
|
||||
PROJECT_NAME="XTablo"
|
||||
|
||||
# Backend
|
||||
BACKEND_CORS_ORIGINS="http://localhost"
|
||||
SECRET_KEY=local_dev
|
||||
FIRST_SUPERUSER=baptiste@xtablo.com
|
||||
FIRST_SUPERUSER_PASSWORD=admin12345_gxydlksjwqnlk
|
||||
|
||||
# run `supabase status`
|
||||
# API URL
|
||||
SUPABASE_URL=https://mhcafqvzbrrwvahpvvzd.supabase.co
|
||||
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDEyNDEzMjEsImV4cCI6MjA1NjgxNzMyMX0.Otxn5BWCPD2ABlMM59hCgeur9Tf_Q7PndAbTkqXDPtM
|
||||
# service_role key
|
||||
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im1oY2FmcXZ6YnJyd3ZhaHB2dnpkIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc0MTI0MTMyMSwiZXhwIjoyMDU2ODE3MzIxfQ.9r33CUsu6ZR4vyv4ed-UY6cLE1FZzSSxTNE8pFUKjN4
|
||||
|
||||
# Postgres
|
||||
# DB URL: postgresql://postgres:postgres@localhost:54322/postgres
|
||||
POSTGRES_SERVER=localhost
|
||||
POSTGRES_PORT=54322
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,16 +0,0 @@
|
|||
from pydantic_settings import BaseSettings
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
# Load environment variables from the .env file
|
||||
env_path = Path(__file__).parent / ".env"
|
||||
load_dotenv(dotenv_path=env_path)
|
||||
|
||||
class Settings(BaseSettings):
|
||||
supabase_url: str = os.getenv("SUPABASE_URL")
|
||||
supabase_key: str = os.getenv("SUPABASE_KEY")
|
||||
secret_key: str = os.getenv("SECRET_KEY")
|
||||
access_token_expire_minutes: int = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))
|
||||
|
||||
settings = Settings()
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
import random
|
||||
import inspect
|
||||
from typing import Annotated, Dict, List, Optional
|
||||
from contextlib import contextmanager
|
||||
|
||||
from app.routers.auth import get_supabase, router as auth_router
|
||||
from fastapi import FastAPI, Depends, HTTPException, status, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from pydantic import BaseModel, EmailStr, field_validator, ValidationInfo, Field, SecretStr
|
||||
from pydantic_core.core_schema import FieldValidationInfo
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from supabase import Client
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(title="XTablo API")
|
||||
|
||||
# CORS Middleware
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:5173"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["X-Error-Code", "X-Error-Message"]
|
||||
)
|
||||
|
||||
app.include_router(auth_router, prefix="/auth")
|
||||
|
||||
# Security
|
||||
security = HTTPBearer()
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
errors = exc.errors()
|
||||
|
||||
custom_errors = []
|
||||
for error in errors:
|
||||
custom_message = error["msg"]
|
||||
if custom_message.startswith("Value error, "):
|
||||
custom_message = custom_message.split(", ")[1]
|
||||
|
||||
custom_errors.append({
|
||||
**error,
|
||||
"form_location": error["loc"][-1],
|
||||
"human_error": custom_message
|
||||
})
|
||||
|
||||
return JSONResponse(status_code=422, content=jsonable_encoder(custom_errors))
|
||||
|
||||
|
||||
|
||||
@app.get("/ping")
|
||||
async def ping():
|
||||
"""Health check endpoint that returns a success status."""
|
||||
return {"status": "success", "message": "API is running"}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,83 +0,0 @@
|
|||
from fastapi import Depends, HTTPException, status, Request
|
||||
from fastapi.routing import APIRouter
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from fastapi.responses import RedirectResponse
|
||||
from supabase import Client
|
||||
from app.config import settings
|
||||
from jose import JWTError, jwt
|
||||
import os
|
||||
from app.routers.helpers import get_supabase, get_current_user_required, get_current_user_optional
|
||||
from app.schemas.user import UserCreate, UserLogin, UserOut
|
||||
from app.schemas.token import RefreshResponse, RefreshToken
|
||||
|
||||
router = APIRouter(tags=["auth"])
|
||||
|
||||
@router.get("/login/google")
|
||||
async def login_with_google(supabase: Client = Depends(get_supabase)):
|
||||
try:
|
||||
response = supabase.auth.sign_in_with_oauth({
|
||||
"provider": "google",
|
||||
"options": {
|
||||
"redirect_to": "http://localhost:8000/auth/callback"
|
||||
}
|
||||
})
|
||||
return {"auth_url": response.url}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
@router.get("/callback")
|
||||
async def google_callback(request: Request, supabase: Client = Depends(get_supabase)):
|
||||
code = request.query_params.get("code")
|
||||
if not code:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Missing authorization code")
|
||||
|
||||
supabase.auth.exchange_code_for_session({"auth_code": code})
|
||||
return RedirectResponse(url="http://localhost:5173")
|
||||
|
||||
@router.get("/users/me")
|
||||
async def get_me(
|
||||
user = Depends(get_current_user_required),
|
||||
):
|
||||
try:
|
||||
return jsonable_encoder(user)
|
||||
except IndexError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found in database"
|
||||
)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=str(e)
|
||||
)
|
||||
|
||||
@router.post("/refresh", response_model=RefreshResponse)
|
||||
async def refresh_token(refresh_request: RefreshToken, supabase: Client = Depends(get_supabase)):
|
||||
"""Refresh the access token using a valid refresh token."""
|
||||
try:
|
||||
# Validate the refresh token and get new tokens
|
||||
response = supabase.auth.refresh_session(refresh_request.refresh_token)
|
||||
|
||||
# Extract user data
|
||||
user_data = {
|
||||
"id": response.user.id,
|
||||
"email": response.user.email,
|
||||
"first_name": response.user.user_metadata.get("first_name", "Unknown"),
|
||||
"last_name": response.user.user_metadata.get("last_name", "Unknown"),
|
||||
"business_name": response.user.user_metadata.get("business_name", "Unknown")
|
||||
}
|
||||
|
||||
# Return the new tokens and user data
|
||||
return {
|
||||
"access_token": response.session.access_token,
|
||||
"refresh_token": response.session.refresh_token,
|
||||
"expires_at": int(response.session.expires_at),
|
||||
"user": user_data
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Failed to refresh token: {str(e)}")
|
||||
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from typing import Optional
|
||||
from supabase import Client
|
||||
from app.config import settings
|
||||
from supabase import create_client
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login", auto_error=False)
|
||||
|
||||
|
||||
def get_supabase() -> Client:
|
||||
supabase_client = create_client(settings.supabase_url, settings.supabase_key)
|
||||
return supabase_client
|
||||
|
||||
# Updated current user dependency
|
||||
async def get_user_from_token(
|
||||
token: str = Depends(oauth2_scheme),
|
||||
supabase: Client = Depends(get_supabase)
|
||||
):
|
||||
try:
|
||||
# Get user from Supabase auth
|
||||
return supabase.auth.get_user(token)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
def get_current_user_required(user: Optional[dict] = Depends(get_user_from_token)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not user:
|
||||
raise credentials_exception
|
||||
|
||||
return user
|
||||
|
||||
def get_current_user_optional(
|
||||
user: Optional[dict] = Depends(get_user_from_token)
|
||||
) -> Optional[dict]:
|
||||
return user
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,11 +0,0 @@
|
|||
from pydantic import BaseModel
|
||||
|
||||
class RefreshToken(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
class RefreshResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
expires_at: int
|
||||
user: dict
|
||||
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
from pydantic import BaseModel, EmailStr, field_validator, Field, SecretStr
|
||||
from pydantic_core.core_schema import FieldValidationInfo
|
||||
|
||||
class UserCreate(BaseModel):
|
||||
email: EmailStr
|
||||
first_name: str
|
||||
last_name: str
|
||||
password: str
|
||||
confirm_password: str
|
||||
business_name: str
|
||||
|
||||
@field_validator("email")
|
||||
def email_must_contain_at_symbol(cls, v):
|
||||
if '@' not in v:
|
||||
raise ValueError("Entrer un email valide")
|
||||
return v
|
||||
|
||||
@field_validator("password")
|
||||
def password_must_contain_at_least_8_characters(cls, v):
|
||||
if len(v) < 8:
|
||||
raise ValueError("Le mot de passe doit contenir au moins 8 caractères")
|
||||
return v
|
||||
|
||||
@field_validator("business_name")
|
||||
def business_name_must_contain_at_least_3_characters(cls, v):
|
||||
if len(v) < 3:
|
||||
raise ValueError("Le nom de la société doit contenir au moins 3 caractères")
|
||||
return v
|
||||
|
||||
@field_validator('confirm_password', mode='before')
|
||||
def passwords_match(cls, v, info: FieldValidationInfo):
|
||||
if 'password' in info.data and v != info.data['password']:
|
||||
raise ValueError('Les mots de passe ne correspondent pas')
|
||||
return v
|
||||
|
||||
|
||||
class UserLogin(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
class UserOut(BaseModel):
|
||||
email: EmailStr
|
||||
business_name: str
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
[project]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
description = "XTablo API"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.115.11",
|
||||
"pydantic-settings>=2.8.1",
|
||||
"python-jose>=3.4.0",
|
||||
"supabase>=2.13.0",
|
||||
]
|
||||
1084
backend/uv.lock
1084
backend/uv.lock
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue