Skip to content

Commit 5f763c0

Browse files
ImShyMiketailhaver
authored andcommitted
auth stuff with separate permissions
1 parent 3baf741 commit 5f763c0

File tree

6 files changed

+78
-43
lines changed

6 files changed

+78
-43
lines changed

api/auth/__init__.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,25 @@
44
SessionClientRequest,
55
OtpClientResponse,
66
generate_session_id,
7-
is_user_admin,
87
is_user_authenticated,
9-
is_user_reviewer,
108
refresh_token,
11-
require_admin,
129
require_auth,
13-
require_reviewer,
1410
router,
1511
send_otp,
1612
validate_otp,
13+
permission_dependency,
1714
)
1815

1916
__all__ = [
2017
"OtpClientRequest",
2118
"SessionClientRequest",
2219
"OtpClientResponse",
2320
"generate_session_id",
24-
"is_user_admin",
2521
"is_user_authenticated",
26-
"is_user_reviewer",
2722
"refresh_token",
28-
"require_admin",
2923
"require_auth",
30-
"require_reviewer",
3124
"router",
3225
"send_otp",
3326
"validate_otp",
27+
"permission_dependency",
3428
]

api/auth/main.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import secrets
55
from datetime import datetime, timedelta, timezone
66
from email.message import EmailMessage
7+
from enum import Enum
78
from functools import wraps
89

910
import aiosmtplib
@@ -18,6 +19,7 @@
1819
from fastapi.responses import RedirectResponse
1920
from pydantic import BaseModel, field_validator
2021
from sqlalchemy.ext.asyncio import AsyncSession
22+
from sqlalchemy import select
2123

2224
from db import get_db
2325
from models.user import User
@@ -28,6 +30,12 @@
2830
r = redis.Redis(password=os.getenv("REDIS_PASSWORD", ""), host=HOST)
2931

3032

33+
class Permission(Enum):
34+
"""User permissions"""
35+
36+
ADMIN = 0
37+
38+
3139
class OtpClientRequest(BaseModel):
3240
"""OTP send request from client"""
3341

@@ -115,44 +123,51 @@ async def wrapper(request: Request, *args, **kwargs):
115123
# return wrapper
116124

117125

118-
# @decorator
119-
def require_admin(func):
120-
"""Require admin status"""
121-
122-
async def wrapper(*args, request: Request, **kwargs):
123-
if not await is_user_authenticated(request) or not await is_user_admin():
124-
return RedirectResponse("/login", status_code=418)
125-
elif await is_user_authenticated(request) and not await is_user_admin():
126-
return RedirectResponse("/home", status_code=403)
127-
return await func(request, *args, **kwargs)
126+
def permission_dependency(permission: Permission):
127+
"""Permission dependency"""
128128

129-
return wrapper
129+
async def verifier(
130+
request: Request,
131+
session: AsyncSession = Depends(get_db),
132+
):
133+
payload = await is_user_authenticated(request)
134+
result = await session.execute(select(User).where(User.email == payload["sub"]))
135+
user = result.scalar_one_or_none()
136+
if not user:
137+
raise HTTPException(status_code=401)
138+
if permission.value not in (user.permissions or []):
139+
raise HTTPException(status_code=403)
140+
request.state.user = user
141+
142+
return verifier
130143

131144

132145
# @decorator
133-
def require_reviewer(func):
134-
"""Require reviewer status"""
135-
136-
async def wrapper(*args, request: Request, **kwargs):
137-
if not await is_user_authenticated(request) or not await is_user_reviewer():
138-
return RedirectResponse("/login", status_code=418)
139-
elif await is_user_authenticated(request) and not await is_user_reviewer():
140-
return RedirectResponse("/home", status_code=403)
141-
return await func(request, *args, **kwargs)
146+
# def require_admin(func):
147+
# """Require admin status"""
142148

143-
return wrapper
149+
# async def wrapper(*args, request: Request, **kwargs):
150+
# if not await is_user_authenticated(request) or not await is_user_admin():
151+
# return RedirectResponse("/login", status_code=418)
152+
# elif await is_user_authenticated(request) and not await is_user_admin():
153+
# return RedirectResponse("/home", status_code=403)
154+
# return await func(request, *args, **kwargs)
144155

156+
# return wrapper
145157

146-
async def is_user_admin() -> bool:
147-
"""Check if user is an admin"""
148-
# TODO: admin check
149-
return False
150158

159+
# @decorator
160+
# def require_reviewer(func):
161+
# """Require reviewer status"""
151162

152-
async def is_user_reviewer() -> bool:
153-
"""Check if user is a reviewer"""
154-
# TODO: reviewer check
155-
return False
163+
# async def wrapper(*args, request: Request, **kwargs):
164+
# if not await is_user_authenticated(request) or not await is_user_reviewer():
165+
# return RedirectResponse("/login", status_code=418)
166+
# elif await is_user_authenticated(request) and not await is_user_reviewer():
167+
# return RedirectResponse("/home", status_code=403)
168+
# return await func(request, *args, **kwargs)
169+
170+
# return wrapper
156171

157172

158173
async def is_user_authenticated(request: Request) -> dict:

api/projects/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ async def create_project(
8686

8787
return {"success": True}
8888

89+
8990
# async def run():
9091
# conn = await asyncpg.connect(user='user', password='password',
9192
# database='database', host='127.0.0.1')

api/users/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
# import asyncpg
77
# import orjson
88
# import sqlalchemy
9-
from fastapi import APIRouter # , Depends, HTTPException, Request
9+
from fastapi import APIRouter # , Depends, HTTPException, Request
1010
# from pydantic import BaseModel
1111
# from sqlalchemy.ext.asyncio import AsyncSession
1212
# from sqlalchemy.orm import selectinload
@@ -36,6 +36,7 @@ async def get_user():
3636
"""Get user details"""
3737
# TODO: implement get user functionality
3838

39+
3940
# @protect
4041
async def delete_user(): # can only delete their own user!!! don't let them delete other users!!!
4142
"""Delete a user account"""

main.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
# import orjson
99
# import os
1010
import dotenv
11-
from fastapi import FastAPI, HTTPException, Request # , Form, Depends
11+
from fastapi import Depends, FastAPI, HTTPException, Request # , Form
1212
from fastapi.exceptions import RequestValidationError
1313
from fastapi.responses import FileResponse, HTMLResponse # , RedirectResponse
1414
from fastapi.staticfiles import StaticFiles
@@ -25,8 +25,9 @@
2525
# from sqlalchemy.ext.asyncio import async_sessionmaker
2626
from api.auth import require_auth # , is_user_authenticated
2727
from api.auth import router as auth_router
28-
from api.users import router as users_router
28+
from api.auth.main import Permission, permission_dependency
2929
from api.projects import router as projects_router
30+
from api.users import router as users_router
3031
from db import engine # , get_db
3132
from models.user import Base
3233

@@ -77,6 +78,8 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
7778
status_code=400,
7879
detail={"errors": exc.errors(), "body": exc.body},
7980
)
81+
82+
8083
app.include_router(auth_router)
8184
app.include_router(users_router)
8285
app.include_router(projects_router)
@@ -103,7 +106,7 @@ async def protected_route(request: Request):
103106
"""Protected route example"""
104107
user_email = request.state.user["sub"]
105108
return HTMLResponse(
106-
f"<h1>Hello World! This is authenticated! Your email is {user_email}! <br>" \
109+
f"<h1>Hello World! This is authenticated! Your email is {user_email}! <br>"
107110
f"Your full string should be {request.state.user}</h1>"
108111
)
109112

@@ -120,6 +123,14 @@ async def serve_projects_test(_request: Request):
120123
return FileResponse("static/projectstest.html")
121124

122125

126+
@app.get("/admin")
127+
async def serve_admin(
128+
_request: Request, _permission=Depends(permission_dependency(Permission.ADMIN))
129+
):
130+
"""Admin page"""
131+
return "test"
132+
133+
123134
# @app.post("/login")
124135
# async def handle_login(email: Annotated[str, Form()], otp: Annotated[int, Form()]):
125136
# pass

models/user.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
"""User database models"""
2+
23
from datetime import datetime, timezone
34

4-
from sqlalchemy import JSON, Column, DateTime, Float, ForeignKey, Integer, String
5+
from sqlalchemy import (
6+
ARRAY,
7+
JSON,
8+
Column,
9+
DateTime,
10+
Float,
11+
ForeignKey,
12+
Integer,
13+
SmallInteger,
14+
String,
15+
)
516
from sqlalchemy.orm import declarative_base, relationship
617

718
Base = declarative_base()
@@ -12,7 +23,9 @@ class User(Base):
1223

1324
__tablename__ = "users"
1425

15-
email = Column(String, primary_key=True)
26+
id = Column(Integer, autoincrement=True, primary_key=True, unique=True)
27+
email = Column(String, nullable=False, unique=True)
28+
permissions = Column(ARRAY(SmallInteger), nullable=False, server_default="{}")
1629
projects = relationship(
1730
"UserProject", back_populates="user", cascade="all, delete-orphan"
1831
)

0 commit comments

Comments
 (0)