auth begin
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
import jwt
|
||||||
|
from jwt.exceptions import PyJWTError
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
import database
|
||||||
|
import models
|
||||||
|
|
||||||
|
|
||||||
|
SECRET = os.getenv("GYM_JWT_SECRET", "dev-secret-change-me")
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
EXPIRE_DAYS = int(os.getenv("GYM_JWT_EXPIRE_DAYS", "30"))
|
||||||
|
|
||||||
|
bearer_scheme = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password: str, hashed: str) -> bool:
|
||||||
|
try:
|
||||||
|
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def create_access_token(user: models.User) -> str:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
payload = {
|
||||||
|
"sub": str(user.id),
|
||||||
|
"username": user.username,
|
||||||
|
"iat": int(now.timestamp()),
|
||||||
|
"exp": int((now + timedelta(days=EXPIRE_DAYS)).timestamp()),
|
||||||
|
}
|
||||||
|
return jwt.encode(payload, SECRET, algorithm=ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_user(
|
||||||
|
credentials: Optional[HTTPAuthorizationCredentials] = Depends(bearer_scheme),
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
) -> models.User:
|
||||||
|
if credentials is None or not credentials.credentials:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Not authenticated",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(credentials.credentials, SECRET, algorithms=[ALGORITHM])
|
||||||
|
user_id = int(payload["sub"])
|
||||||
|
except (PyJWTError, KeyError, ValueError):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Invalid token",
|
||||||
|
)
|
||||||
|
|
||||||
|
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="User not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
Binary file not shown.
+3
-1
@@ -4,6 +4,7 @@ from fastapi import FastAPI
|
|||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from database import engine, Base
|
from database import engine, Base
|
||||||
import routers.clients
|
import routers.clients
|
||||||
|
import routers.auth
|
||||||
import routers.workout_types
|
import routers.workout_types
|
||||||
import routers.workouts
|
import routers.workouts
|
||||||
import routers.exercises
|
import routers.exercises
|
||||||
@@ -29,6 +30,7 @@ app.add_middleware(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Подключаем роутеры
|
# Подключаем роутеры
|
||||||
|
app.include_router(routers.auth.router)
|
||||||
app.include_router(routers.clients.router)
|
app.include_router(routers.clients.router)
|
||||||
app.include_router(routers.workout_types.router)
|
app.include_router(routers.workout_types.router)
|
||||||
app.include_router(routers.workouts.router)
|
app.include_router(routers.workouts.router)
|
||||||
@@ -39,7 +41,7 @@ app.include_router(routers.sync.router)
|
|||||||
@app.get("/")
|
@app.get("/")
|
||||||
def root():
|
def root():
|
||||||
return {
|
return {
|
||||||
"message": "Gym Tracker API",
|
"message": "Gym Bro API",
|
||||||
"docs": "/docs",
|
"docs": "/docs",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+156
-31
@@ -1,22 +1,27 @@
|
|||||||
from sqlalchemy import Column, Integer, String, Float, ForeignKey, CheckConstraint, Index
|
from sqlalchemy import (
|
||||||
|
Column,
|
||||||
|
Integer,
|
||||||
|
String,
|
||||||
|
Float,
|
||||||
|
ForeignKey,
|
||||||
|
CheckConstraint,
|
||||||
|
UniqueConstraint,
|
||||||
|
)
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from database import Base
|
from database import Base
|
||||||
|
|
||||||
class Client(Base):
|
|
||||||
__tablename__ = "clients"
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
username = Column(String, unique=True, nullable=False, index=True)
|
||||||
full_name = Column(String, nullable=False)
|
password_hash = Column(String, nullable=False)
|
||||||
birth_date = Column(String)
|
display_name = Column(String, default="")
|
||||||
goal = Column(String, default="")
|
role = Column(String, default="personal") # personal | trainer
|
||||||
notes = Column(String, default="")
|
|
||||||
is_active = Column(Integer, default=1)
|
|
||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
updated_at = Column(String)
|
|
||||||
|
|
||||||
workouts = relationship("Workout", back_populates="client")
|
|
||||||
body_metrics = relationship("BodyMetric", back_populates="client")
|
|
||||||
|
|
||||||
class WorkoutType(Base):
|
class WorkoutType(Base):
|
||||||
__tablename__ = "workout_types"
|
__tablename__ = "workout_types"
|
||||||
@@ -30,12 +35,22 @@ class WorkoutType(Base):
|
|||||||
exercises = relationship("Exercise", back_populates="workout_type")
|
exercises = relationship("Exercise", back_populates="workout_type")
|
||||||
workouts = relationship("Workout", back_populates="workout_type")
|
workouts = relationship("Workout", back_populates="workout_type")
|
||||||
|
|
||||||
|
|
||||||
class Exercise(Base):
|
class Exercise(Base):
|
||||||
__tablename__ = "exercises"
|
__tablename__ = "exercises"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
workout_type_id = Column(Integer, ForeignKey("workout_types.id", ondelete="SET NULL"))
|
|
||||||
|
# NULL = глобальный справочник, иначе — личное упражнение пользователя
|
||||||
|
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
|
||||||
|
|
||||||
|
workout_type_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("workout_types.id", ondelete="SET NULL"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
name = Column(String, nullable=False, index=True)
|
name = Column(String, nullable=False, index=True)
|
||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
updated_at = Column(String)
|
updated_at = Column(String)
|
||||||
@@ -43,46 +58,99 @@ class Exercise(Base):
|
|||||||
workout_type = relationship("WorkoutType", back_populates="exercises")
|
workout_type = relationship("WorkoutType", back_populates="exercises")
|
||||||
workout_exercises = relationship("WorkoutExercise", back_populates="exercise")
|
workout_exercises = relationship("WorkoutExercise", back_populates="exercise")
|
||||||
|
|
||||||
|
|
||||||
|
class Client(Base):
|
||||||
|
__tablename__ = "clients"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
|
||||||
|
|
||||||
|
full_name = Column(String, nullable=False)
|
||||||
|
birth_date = Column(String)
|
||||||
|
goal = Column(String, default="")
|
||||||
|
notes = Column(String, default="")
|
||||||
|
color = Column(String)
|
||||||
|
is_active = Column(Integer, default=1)
|
||||||
|
|
||||||
|
created_at = Column(String)
|
||||||
|
updated_at = Column(String)
|
||||||
|
|
||||||
|
workouts = relationship("Workout", back_populates="client")
|
||||||
|
body_metrics = relationship("BodyMetric", back_populates="client")
|
||||||
|
|
||||||
|
|
||||||
class Workout(Base):
|
class Workout(Base):
|
||||||
__tablename__ = "workouts"
|
__tablename__ = "workouts"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
client_id = Column(Integer, ForeignKey("clients.id", ondelete="SET NULL"))
|
|
||||||
workout_type_id = Column(Integer, ForeignKey("workout_types.id", ondelete="SET NULL"))
|
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
|
||||||
|
client_id = Column(Integer, ForeignKey("clients.id", ondelete="SET NULL"), index=True)
|
||||||
|
workout_type_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("workout_types.id", ondelete="SET NULL"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
start_time = Column(String, nullable=False)
|
start_time = Column(String, nullable=False)
|
||||||
end_time = Column(String)
|
end_time = Column(String)
|
||||||
notes = Column(String, default="")
|
notes = Column(String, default="")
|
||||||
synced = Column(Integer, default=0)
|
synced = Column(Integer, default=0)
|
||||||
|
|
||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
updated_at = Column(String)
|
updated_at = Column(String)
|
||||||
|
|
||||||
client = relationship("Client", back_populates="workouts")
|
client = relationship("Client", back_populates="workouts")
|
||||||
workout_type = relationship("WorkoutType", back_populates="workouts")
|
workout_type = relationship("WorkoutType", back_populates="workouts")
|
||||||
workout_exercises = relationship("WorkoutExercise", back_populates="workout", cascade="all, delete-orphan")
|
|
||||||
cardio_sessions = relationship("Cardio", back_populates="workout", cascade="all, delete-orphan")
|
workout_exercises = relationship(
|
||||||
|
"WorkoutExercise",
|
||||||
|
back_populates="workout",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
cardio_sessions = relationship(
|
||||||
|
"Cardio",
|
||||||
|
back_populates="workout",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WorkoutExercise(Base):
|
class WorkoutExercise(Base):
|
||||||
__tablename__ = "workout_exercises"
|
__tablename__ = "workout_exercises"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
workout_id = Column(Integer, ForeignKey("workouts.id", ondelete="CASCADE"))
|
|
||||||
exercise_id = Column(Integer, ForeignKey("exercises.id", ondelete="SET NULL"))
|
workout_id = Column(Integer, ForeignKey("workouts.id", ondelete="CASCADE"), index=True)
|
||||||
exercise_name = Column(String, nullable=False)
|
exercise_id = Column(Integer, ForeignKey("exercises.id", ondelete="SET NULL"), index=True)
|
||||||
|
|
||||||
|
exercise_name = Column(String, nullable=False, index=True)
|
||||||
sort_order = Column(Integer, default=0)
|
sort_order = Column(Integer, default=0)
|
||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
|
|
||||||
workout = relationship("Workout", back_populates="workout_exercises")
|
workout = relationship("Workout", back_populates="workout_exercises")
|
||||||
exercise = relationship("Exercise", back_populates="workout_exercises")
|
exercise = relationship("Exercise", back_populates="workout_exercises")
|
||||||
sets = relationship("Set", back_populates="workout_exercise", cascade="all, delete-orphan")
|
sets = relationship(
|
||||||
|
"Set",
|
||||||
|
back_populates="workout_exercise",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Set(Base):
|
class Set(Base):
|
||||||
__tablename__ = "sets"
|
__tablename__ = "sets"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
workout_exercise_id = Column(Integer, ForeignKey("workout_exercises.id", ondelete="CASCADE"))
|
|
||||||
|
workout_exercise_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("workout_exercises.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
is_dropset = Column(Integer, default=0)
|
is_dropset = Column(Integer, default=0)
|
||||||
weight = Column(Float)
|
weight = Column(Float)
|
||||||
reps = Column(Integer)
|
reps = Column(Integer)
|
||||||
@@ -93,51 +161,73 @@ class Set(Base):
|
|||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
|
|
||||||
workout_exercise = relationship("WorkoutExercise", back_populates="sets")
|
workout_exercise = relationship("WorkoutExercise", back_populates="sets")
|
||||||
dropset_items = relationship("DropsetItem", back_populates="set", cascade="all, delete-orphan")
|
dropset_items = relationship(
|
||||||
|
"DropsetItem",
|
||||||
|
back_populates="set",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
CheckConstraint(
|
CheckConstraint(
|
||||||
"(is_dropset = 0 AND weight IS NOT NULL AND reps IS NOT NULL) OR "
|
"(is_dropset = 0 AND weight IS NOT NULL AND reps IS NOT NULL) OR "
|
||||||
"(is_dropset = 1 AND weight IS NULL AND reps IS NULL)",
|
"(is_dropset = 1 AND weight IS NULL AND reps IS NULL)",
|
||||||
name="check_set_data"
|
name="ck_sets_dropset_shape",
|
||||||
|
),
|
||||||
|
CheckConstraint(
|
||||||
|
"rating IS NULL OR (rating >= 1 AND rating <= 5)",
|
||||||
|
name="ck_sets_rating",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DropsetItem(Base):
|
class DropsetItem(Base):
|
||||||
__tablename__ = "dropset_items"
|
__tablename__ = "dropset_items"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
set_id = Column(Integer, ForeignKey("sets.id", ondelete="CASCADE"))
|
set_id = Column(Integer, ForeignKey("sets.id", ondelete="CASCADE"), index=True)
|
||||||
|
|
||||||
weight = Column(Float, nullable=False)
|
weight = Column(Float, nullable=False)
|
||||||
reps = Column(Integer, nullable=False)
|
reps = Column(Integer, nullable=False)
|
||||||
sort_order = Column(Integer, default=0)
|
sort_order = Column(Integer, default=0)
|
||||||
|
|
||||||
set = relationship("Set", back_populates="dropset_items")
|
set = relationship("Set", back_populates="dropset_items")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint("weight > 0", name="ck_dropset_weight_positive"),
|
||||||
|
CheckConstraint("reps > 0", name="ck_dropset_reps_positive"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Cardio(Base):
|
class Cardio(Base):
|
||||||
__tablename__ = "cardio"
|
__tablename__ = "cardio"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
workout_id = Column(Integer, ForeignKey("workouts.id", ondelete="CASCADE"))
|
|
||||||
|
workout_id = Column(Integer, ForeignKey("workouts.id", ondelete="CASCADE"), index=True)
|
||||||
|
|
||||||
cardio_type = Column(String, nullable=False)
|
cardio_type = Column(String, nullable=False)
|
||||||
purpose = Column(String, nullable=False)
|
purpose = Column(String, nullable=False) # warmup | cooldown | separate | другое
|
||||||
|
|
||||||
duration = Column(Integer)
|
duration = Column(Integer)
|
||||||
distance = Column(Float)
|
distance = Column(Float)
|
||||||
speed = Column(Float)
|
speed = Column(Float)
|
||||||
incline = Column(Float)
|
incline = Column(Float)
|
||||||
resistance = Column(Integer)
|
resistance = Column(Integer)
|
||||||
|
|
||||||
notes = Column(String, default="")
|
notes = Column(String, default="")
|
||||||
sort_order = Column(Integer, default=0)
|
sort_order = Column(Integer, default=0)
|
||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
|
|
||||||
workout = relationship("Workout", back_populates="cardio_sessions")
|
workout = relationship("Workout", back_populates="cardio_sessions")
|
||||||
|
|
||||||
|
|
||||||
class MetricType(Base):
|
class MetricType(Base):
|
||||||
__tablename__ = "metric_types"
|
__tablename__ = "metric_types"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
|
|
||||||
name = Column(String, unique=True, nullable=False)
|
name = Column(String, unique=True, nullable=False)
|
||||||
unit = Column(String, default="см")
|
unit = Column(String, default="см")
|
||||||
category = Column(String, default="body")
|
category = Column(String, default="body")
|
||||||
@@ -146,26 +236,61 @@ class MetricType(Base):
|
|||||||
|
|
||||||
metric_values = relationship("BodyMetricValue", back_populates="metric_type")
|
metric_values = relationship("BodyMetricValue", back_populates="metric_type")
|
||||||
|
|
||||||
|
|
||||||
class BodyMetric(Base):
|
class BodyMetric(Base):
|
||||||
__tablename__ = "body_metrics"
|
__tablename__ = "body_metrics"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
sync_id = Column(String, unique=True, nullable=False, index=True)
|
sync_id = Column(String, unique=True, nullable=False, index=True)
|
||||||
client_id = Column(Integer, ForeignKey("clients.id", ondelete="SET NULL"))
|
|
||||||
|
owner_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), index=True)
|
||||||
|
client_id = Column(Integer, ForeignKey("clients.id", ondelete="SET NULL"), index=True)
|
||||||
|
|
||||||
metric_date = Column(String, nullable=False)
|
metric_date = Column(String, nullable=False)
|
||||||
notes = Column(String, default="")
|
notes = Column(String, default="")
|
||||||
created_at = Column(String)
|
created_at = Column(String)
|
||||||
|
|
||||||
client = relationship("Client", back_populates="body_metrics")
|
client = relationship("Client", back_populates="body_metrics")
|
||||||
values = relationship("BodyMetricValue", back_populates="body_metric", cascade="all, delete-orphan")
|
values = relationship(
|
||||||
|
"BodyMetricValue",
|
||||||
|
back_populates="body_metric",
|
||||||
|
cascade="all, delete-orphan",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BodyMetricValue(Base):
|
class BodyMetricValue(Base):
|
||||||
__tablename__ = "body_metric_values"
|
__tablename__ = "body_metric_values"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
body_metric_id = Column(Integer, ForeignKey("body_metrics.id", ondelete="CASCADE"))
|
|
||||||
metric_type_id = Column(Integer, ForeignKey("metric_types.id", ondelete="CASCADE"))
|
body_metric_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("body_metrics.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
metric_type_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("metric_types.id", ondelete="CASCADE"),
|
||||||
|
index=True,
|
||||||
|
)
|
||||||
|
|
||||||
value = Column(Float, nullable=False)
|
value = Column(Float, nullable=False)
|
||||||
|
|
||||||
body_metric = relationship("BodyMetric", back_populates="values")
|
body_metric = relationship("BodyMetric", back_populates="values")
|
||||||
metric_type = relationship("MetricType", back_populates="metric_values")
|
metric_type = relationship("MetricType", back_populates="metric_values")
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"body_metric_id",
|
||||||
|
"metric_type_id",
|
||||||
|
name="uq_body_metric_value_type",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SyncLog(Base):
|
||||||
|
__tablename__ = "sync_log"
|
||||||
|
|
||||||
|
device_id = Column(String, primary_key=True)
|
||||||
|
last_sync = Column(String, nullable=False)
|
||||||
|
updated_at = Column(String)
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ uvicorn[standard]==0.24.0
|
|||||||
sqlalchemy==2.0.23
|
sqlalchemy==2.0.23
|
||||||
pydantic==2.5.0
|
pydantic==2.5.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
|
PyJWT==2.8.0
|
||||||
|
bcrypt==4.1.2
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
import auth
|
||||||
|
import database
|
||||||
|
import models
|
||||||
|
import schemas
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_timestamp() -> str:
|
||||||
|
return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%fZ")
|
||||||
|
|
||||||
|
|
||||||
|
def user_to_out(user: models.User) -> schemas.UserOut:
|
||||||
|
return schemas.UserOut(
|
||||||
|
id=user.id,
|
||||||
|
username=user.username,
|
||||||
|
display_name=user.display_name or user.username,
|
||||||
|
role=user.role or "personal",
|
||||||
|
created_at=user.created_at or get_current_timestamp(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/register", response_model=schemas.Token)
|
||||||
|
def register(
|
||||||
|
data: schemas.UserCreate,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
):
|
||||||
|
username = (data.username or "").strip().lower()
|
||||||
|
password = data.password or ""
|
||||||
|
role = data.role or "personal"
|
||||||
|
|
||||||
|
if len(username) < 3:
|
||||||
|
raise HTTPException(status_code=400, detail="Username must be at least 3 characters")
|
||||||
|
|
||||||
|
if len(password) < 6:
|
||||||
|
raise HTTPException(status_code=400, detail="Password must be at least 6 characters")
|
||||||
|
|
||||||
|
if role not in ("personal", "trainer"):
|
||||||
|
role = "personal"
|
||||||
|
|
||||||
|
exists = db.query(models.User).filter(models.User.username == username).first()
|
||||||
|
if exists:
|
||||||
|
raise HTTPException(status_code=400, detail="Username already exists")
|
||||||
|
|
||||||
|
user = models.User(
|
||||||
|
username=username,
|
||||||
|
password_hash=auth.hash_password(password),
|
||||||
|
display_name=(data.display_name or username).strip(),
|
||||||
|
role=role,
|
||||||
|
created_at=get_current_timestamp(),
|
||||||
|
)
|
||||||
|
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(user)
|
||||||
|
|
||||||
|
token = auth.create_access_token(user)
|
||||||
|
|
||||||
|
return schemas.Token(
|
||||||
|
access_token=token,
|
||||||
|
token_type="bearer",
|
||||||
|
user=user_to_out(user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/login", response_model=schemas.Token)
|
||||||
|
def login(
|
||||||
|
data: schemas.UserLogin,
|
||||||
|
db: Session = Depends(database.get_db),
|
||||||
|
):
|
||||||
|
username = (data.username or "").strip().lower()
|
||||||
|
password = data.password or ""
|
||||||
|
|
||||||
|
user = db.query(models.User).filter(models.User.username == username).first()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||||
|
|
||||||
|
if not auth.verify_password(password, user.password_hash):
|
||||||
|
raise HTTPException(status_code=401, detail="Invalid username or password")
|
||||||
|
|
||||||
|
token = auth.create_access_token(user)
|
||||||
|
|
||||||
|
return schemas.Token(
|
||||||
|
access_token=token,
|
||||||
|
token_type="bearer",
|
||||||
|
user=user_to_out(user),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=schemas.UserOut)
|
||||||
|
def me(
|
||||||
|
current_user: models.User = Depends(auth.get_current_user),
|
||||||
|
):
|
||||||
|
return user_to_out(current_user)
|
||||||
@@ -5,8 +5,13 @@ import database
|
|||||||
import models
|
import models
|
||||||
import schemas
|
import schemas
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import auth
|
||||||
|
|
||||||
router = APIRouter(prefix="/workout-types", tags=["workout-types"])
|
router = APIRouter(
|
||||||
|
prefix="/workout-types",
|
||||||
|
tags=["workout-types"],
|
||||||
|
dependencies=[Depends(auth.get_current_user)],
|
||||||
|
)
|
||||||
|
|
||||||
def get_current_timestamp():
|
def get_current_timestamp():
|
||||||
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
|
return datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%fZ')
|
||||||
|
|||||||
@@ -2,6 +2,37 @@ from pydantic import BaseModel, Field
|
|||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# USERS
|
||||||
|
# ============================================================
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
display_name: Optional[str] = ""
|
||||||
|
role: Optional[str] = "personal"
|
||||||
|
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class UserOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
username: str
|
||||||
|
display_name: str
|
||||||
|
role: str
|
||||||
|
created_at: str
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
user: UserOut
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# CLIENTS
|
# CLIENTS
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user